This blog post is mainly inspired by the works of Martin Fowler and Eric Evans in their seminal paper Specifications
I find this pattern geniune as it remedies years of head bashing and hair pulling for finding ways of isolating the evolution of a domain model from the mechanisms involved in inspecting it and querying it while maintaining a high level of encapsulation. I like to think about it as "reflection" on the domain model. Hopefully, things will become clearer as we move proceed.
Let us suppose that we have an ecommerce application with a very exotic product catalog whereby products are represented by a type hierarchy, for example
Suppose we'd like to search for products matching the following criteria:
A naive approach would simply have it done this way
Now suppose we also want to be able to search by resolution and available units. It won't get any better
Clearly, this solution does not scale, plus it forces us to expose some properties of the product, maybe some that we'd rather keep private. What is even worse is the logic that builds up upon these properties far away from where the properties are located. By all design standards, this solution is a time bomb and a maintenance nightmare. Can we do better?
Of course, we can - we wouldn't be here otherwise :-). The solution has been formalized under the name: specification pattern. Basically, we create our search criteria and let the product tell you if it meets them or not. Quoting the master Martin Fowler "Tell, don't ask".
Phew!!! With all of this behind, let's look how the client code would look like
Please admire the conciseness and expressivity of the solution. It is true that we had to put in a lot of code behind, but that's the price if you're willing to use the porcelain instead of the plumbing. Please note that this implementation of the specification pattern is a mashup of several design patters:
I hope that was an interesting read.
Thank you
I find this pattern geniune as it remedies years of head bashing and hair pulling for finding ways of isolating the evolution of a domain model from the mechanisms involved in inspecting it and querying it while maintaining a high level of encapsulation. I like to think about it as "reflection" on the domain model. Hopefully, things will become clearer as we move proceed.
Let us suppose that we have an ecommerce application with a very exotic product catalog whereby products are represented by a type hierarchy, for example
public class Product{
String gTIN;
double price;
int units;
}
public class Television extends Product{
ResolutionEnum resolution;
float screenSize; //in inches
}
String gTIN;
double price;
int units;
}
public class Television extends Product{
ResolutionEnum resolution;
float screenSize; //in inches
}
Suppose we'd like to search for products matching the following criteria:
- The maximum price is 3000 Dh (that is the Moroccan currency dirhams, MAD)
- The screen size is more than 12.1''
A naive approach would simply have it done this way
public class SearchProductService{
ProductRepository inMemoryRepository;
public Product findBy(double maxPrice, float minScreenSize){
List<Product> allProducts = inMemoryRepository.findAll(p-> {p.price < price});
allProduct.stream().filter(p -> p instanceof Television).map( p-> (Television ) p)
.filter(p-> p.resolution > minScreenSize).collect(toList());
}
}ProductRepository inMemoryRepository;
public Product findBy(double maxPrice, float minScreenSize){
List<Product> allProducts = inMemoryRepository.findAll(p-> {p.price < price});
allProduct.stream().filter(p -> p instanceof Television).map( p-> (Television ) p)
.filter(p-> p.resolution > minScreenSize).collect(toList());
}
Now suppose we also want to be able to search by resolution and available units. It won't get any better
public class SearchProductService{
ProductRepository inMemoryRepository;
public Product findBy(double minPrice, float maxScreenSize ){
}
public Product findBy(ResolutionEnum resolution, int availableUnits ){
}
public Product findBy(ResolutionEnum resolution, int availableUnits ,double maxPrice, float minScreenSize){
}
//handle all combinations?
}
ProductRepository inMemoryRepository;
public Product findBy(double minPrice, float maxScreenSize ){
}
public Product findBy(ResolutionEnum resolution, int availableUnits ){
}
public Product findBy(ResolutionEnum resolution, int availableUnits ,double maxPrice, float minScreenSize){
}
//handle all combinations?
}
Clearly, this solution does not scale, plus it forces us to expose some properties of the product, maybe some that we'd rather keep private. What is even worse is the logic that builds up upon these properties far away from where the properties are located. By all design standards, this solution is a time bomb and a maintenance nightmare. Can we do better?
Of course, we can - we wouldn't be here otherwise :-). The solution has been formalized under the name: specification pattern. Basically, we create our search criteria and let the product tell you if it meets them or not. Quoting the master Martin Fowler "Tell, don't ask".
class Product{
public boolean satisfies(SearchCriteria criteria){
//Open up for extension
return criteria.isSatisifiedBy(this);
}
}
class Television extends Product{
//No change here }
public boolean satisfies(SearchCriteria criteria){
//Open up for extension
return criteria.isSatisifiedBy(this);
}
}
class Television extends Product{
//No change here }
/*************** Define Criteria ***********************/
public interface SearchCriteria{
boolean isSatisfiedBy(Product product);
}
/**************** Composite **************************/
public class Criteria implements SearchCriteria{
private List<SearchCriteria> criteria ;
public Criteria(List<SearchCriteron> criteria){
this.criteria = criteria;
}
//We could also add the operators add, not, or for combining criteria, here it is AND
public isSatisfiedBy(Product product)(){
Iterator<Criteria> iterator = criteria.iterator();
while(iterator.hasNext()){
if(!iterator.next().isSatisfiedBy(product))
return false;
}
return true:
}
}
/**************** Price Criterion **************************/
public class PriceCriterion implements SearchCritera{
public PriceCriterion(Operator operator, double target){
//
}
public boolean isSatisfiedBy (Product product){
//Put logic here
}
}
/*************** Criteria builder ********************/
public class SearchCriteriaBuilder{
protected List<SearchCriteron> criteria = new ArrayList<>();
private PriceCriteriaBuilder priceCriteriaBuilder;
public PriceCriteriaBuilder withPrice(){
if(priceCriteriaBuilder == null)
priceCriteriaBuilder = new PriceCriteriaBuilder();
return priceCriteriaBuilder;
}
public PriceCriteriaBuilder and(){
return this;
}
public SearchCritera build(){
return new Criteria(criteria);
}
public PriceCriteriaBuilder{
Operator operator;
double targetPrice;
public enum Operator{
lessThan,
equal,
largetThan
....
}
public PriceCriteriaBuilder being(Operator operator) {
this.operator = operator;
return this;
}
public PriceCriteriaBuilder value(double targetPrice) {
this.targetPrice = targetPrice;
PriceCriteriaBuilder.this.criteria.add(new PriceCriterion(operator,targetPrice));
return PriceCriteriaBuilder.this ;
}
}
}
public interface SearchCriteria{
boolean isSatisfiedBy(Product product);
}
/**************** Composite **************************/
public class Criteria implements SearchCriteria{
private List<SearchCriteria> criteria ;
public Criteria(List<SearchCriteron> criteria){
this.criteria = criteria;
}
//We could also add the operators add, not, or for combining criteria, here it is AND
public isSatisfiedBy(Product product)(){
Iterator<Criteria> iterator = criteria.iterator();
while(iterator.hasNext()){
if(!iterator.next().isSatisfiedBy(product))
return false;
}
return true:
}
}
/**************** Price Criterion **************************/
public class PriceCriterion implements SearchCritera{
public PriceCriterion(Operator operator, double target){
//
}
public boolean isSatisfiedBy (Product product){
//Put logic here
}
}
/*************** Criteria builder ********************/
public class SearchCriteriaBuilder{
protected List<SearchCriteron> criteria = new ArrayList<>();
private PriceCriteriaBuilder priceCriteriaBuilder;
public PriceCriteriaBuilder withPrice(){
if(priceCriteriaBuilder == null)
priceCriteriaBuilder = new PriceCriteriaBuilder();
return priceCriteriaBuilder;
}
public PriceCriteriaBuilder and(){
return this;
}
public SearchCritera build(){
return new Criteria(criteria);
}
public PriceCriteriaBuilder{
Operator operator;
double targetPrice;
public enum Operator{
lessThan,
equal,
largetThan
....
}
public PriceCriteriaBuilder being(Operator operator) {
this.operator = operator;
return this;
}
public PriceCriteriaBuilder value(double targetPrice) {
this.targetPrice = targetPrice;
PriceCriteriaBuilder.this.criteria.add(new PriceCriterion(operator,targetPrice));
return PriceCriteriaBuilder.this ;
}
}
}
Phew!!! With all of this behind, let's look how the client code would look like
Criteria criteria = new SearchCriteriaBuilder().
withPrice()
.being(lessThan).value(3000)
.and()
.withScreenSize()//could be impletement in the same manner
.being(largetThan).value(12.1)
.build();
Television television = ProductRepository.getTelevisions(); television.satisfies(criteria);
withPrice()
.being(lessThan).value(3000)
.and()
.withScreenSize()//could be impletement in the same manner
.being(largetThan).value(12.1)
.build();
Television television = ProductRepository.getTelevisions(); television.satisfies(criteria);
Please admire the conciseness and expressivity of the solution. It is true that we had to put in a lot of code behind, but that's the price if you're willing to use the porcelain instead of the plumbing. Please note that this implementation of the specification pattern is a mashup of several design patters:
- Composite Pattern: product deals only with the criteria class and it does not change if there is one criterion or many.
- Visitor Pattern: Each Concerte criteria class has to deal with the specifics of the product, and the interaction with the product is kept simple thanks to one method satisfies(SearchCriteria criteria)
- Command Pattern: We build the list of criteria and execute them once at the end with a single call satisfies(criteria).
- Builder Pattern: Allowed the creation of a fluid, progressive API.
I hope that was an interesting read.
Thank you
6 comments:
Thanks for the example. I have created a working code example using the snippets you provide at https://github.com/tusharjoshi/java-specificiation-pattern
Thanks again.
The correct link is https://github.com/tusharjoshi/java-specification-pattern
The next step is to use Kotlin DSL for your specifications and throw the old-fashioned patterns away.
These are contradictory statements:
"Please admire the conciseness and expressivity of the solution. It is true that we had to put in a lot of code behind..."
The solution is ALL of the code. When taken as a whole, there's a lot of code that was written to accomplish this supposedly "concise" solution.
I'm in fond of clear, beautiful OOP, but this pattern is simply senseless.
Just one reason - in 99.99% of real ecommerce applications you cannnot do Repository.getAllEntities() to apply your idea to an uploaded List. You could have a tremendous amount of data in your data base and any .getAllXxx() functionality is not only useless but can be even harmful. That's why in real world you almost always have to make a parameterized SQL\JPQL\Other query to your underlying data storage. Moreover, you always have a predefined set of business operations and flows, so there is not real need in such a flexible tool, not even saying that this tool is not applicable to real world search.
Thanks, interesting approach. But for me, OO means information hiding in the first place. And in the presented code above the class Product does not hide it's information at all.
And that's the interesting part. How to design that in order to fullfill encapsulation in both classes, Product and Criteria.
Post a Comment