In my previous post,
An idea for Composite Specifications, we took a look at how a composite specification could be structured and designed.
Now, we'll see how to concretely implement it, using (as always here in my blog, where not specified) the Java language.
Let us rewrite the composite specification interfaces:
public interface Specification<O> {
public boolean evaluate(O o);
}
public interface CompositeSpecification<S, O> extends Specification<O> {
public CompositeSpecification compose(S specification);
public CompositeSpecification and(CompositeSpecification<S, O> specification);
public CompositeSpecification and(S specification);
public CompositeSpecification andNot(CompositeSpecification<S, O> specification);
public CompositeSpecification andNot(S specification);
public CompositeSpecification or(CompositeSpecification<S, O> specification);
public CompositeSpecification or(S specification);
public CompositeSpecification orNot(CompositeSpecification<S, O> specification);
public CompositeSpecification orNot(S specification);
public boolean evaluate(O object);
}
Here are some
main advantages of such an interface:
- Composition using well known logical operators.
- Composition of custom specification types, without requiring to implement any particular interface.
- Highly declarative style of composition, thanks to a fluent interface.
The CompositeSpecification let you compose custom specification with other CompositeSpecification: the first thing you need is an adapter for adapting custom types to the
Specification interface:
public class SpecificationGenericAdapter implements Specification {
private Object specification;
private Method specificationMethod;
public SpecificationGenericAdapter(Object specification, Method specificationMethod) {
this.specification = specification;
this.specificationMethod = specificationMethod;
}
public boolean evaluate(Object object) {
boolean result = false;
try {
Object tmp = this.specificationMethod.invoke(specification, object);
result = (Boolean) tmp;
}
catch(Exception ex) {
throw new SpecificationAdapterException("...", ex);
}
return result;
}
}
This adapter encapsulate the object to adapt and use Java reflection to call the method that will actually evaluate the specification.
You may note the possibility to compose with the inverse of a specification, using the
andNot and
orNot operators: this is accomplished simply applying the following decorator:
public class InverseSpecification implements Specification {
private Specification specification;
public InverseSpecification(Specification specification) {
this.specification = specification;
}
public boolean evaluate(Object o) {
return !this.specification.evaluate(o);
}
}
It simply inverts the decorated specification.
Now we need to implement the logical operators.
I've made a first implementation using Apache Commons Predicate: it worked well, was very straightforward, but it was rather inefficient in terms of memory usage.
Due to the Commons Predicates implementation, you have to create a different Predicate object for every operator you use: so, considering a simple CompositeSpecification with just one logical
and operator, used in one thousand different instances of a business object, you'll have one thousand of
AndPredicate just for implementing an
and operator which makes always the same task!
So, here is a
shared implementation of logical operators, which uses the
flyweight pattern; these are the involved classes:
public interface BinaryOperator {
public boolean evaluate(Specification a, Specification b, Object o);
public boolean evaluate(Specification a, boolean b, Object o);
}
public class AndOperator implements BinaryOperator {
public boolean evaluate(Specification a, Specification b, Object o) {
return a.evaluate(o) && b.evaluate(o);
}
public boolean evaluate(Specification a, boolean b, Object o) {
return a.evaluate(o) && b;
}
}
public class OrOperator implements BinaryOperator {
public boolean evaluate(Specification a, Specification b, Object o) {
return a.evaluate(o) || b.evaluate(o);
}
public boolean evaluate(Specification a, boolean b, Object o) {
return a.evaluate(o) || b;
}
}
public class OperatorFactory {
private static final OperatorFactory factory = new OperatorFactory();
private BinaryOperator andOperator = new AndOperator();
private BinaryOperator orOperator = new OrOperator();
private OperatorFactory() {}
public static OperatorFactory getInstance() {
return factory;
}
public BinaryOperator getAndOperator() {
return this.andOperator;
}
public BinaryOperator getOrOperator() {
return this.orOperator;
}
}
You should note the following:
- The BinaryOperator evaluate() method takes as arguments not only the object to evaluate, but also the two specifications to apply (or a specification and an already computed boolean value): this is the so called extrinsic state, that is, state that cannot be shared because context dependent.
- The singleton OperatorFactory must be used in order to obtain shared operator instances: so, you don't have to directly construct operators.
Last, but not least, the CompositeSpecification implementation:
public class CompositeSpecificationImpl<S, O>
implements CompositeSpecification<S, O> {
private Class<S> specificationClass;
private Method specificationMethod;
private OperatorFactory operatorFactory = OperatorFactory.getInstance();
private List<Specification> specificationsList = new LinkedList<Specification>();
private List<BinaryOperator> operatorsList = new LinkedList<BinaryOperator>();
public CompositeSpecificationImpl(Class<S> specificationClass, String specificationMethod) {
this.specificationClass = specificationClass;
try {
Method[] methods = this.specificationClass.getDeclaredMethods();
for (Method m : methods) {
if (m.getName().equals(specificationMethod)) {
this.specificationMethod = m;
break;
}
}
if (this.specificationMethod == null) {
throw new SpecificationDescriptionException("...");
}
}
catch(SecurityException ex) {
throw new SpecificationDescriptionException("...");
}
}
public CompositeSpecification compose(S specification) {
this.specificationsList.clear();
this.operatorsList.clear();
this.specificationsList.add(this.adaptSpecification(specification));
return this;
}
public CompositeSpecification and(S specification) {
if (this.specificationsList.isEmpty()) {
throw new SpecificationNotComposedException("...");
}
this.operatorsList.add(this.operatorFactory.getAndOperator());
this.specificationsList.add(this.adaptSpecification(specification));
return this;
}
public CompositeSpecification or(S specification) {
if (this.specificationsList.isEmpty()) {
throw new SpecificationNotComposedException("...");
}
this.operatorsList.add(this.operatorFactory.getOrOperator());
this.specificationsList.add(this.adaptSpecification(specification));
return this;
}
public CompositeSpecification and(CompositeSpecification<S, O> specification) {
if (!specification.equals(this)) {
throw new IllegalArgumentException("...");
}
if (this.specificationsList.isEmpty()) {
throw new SpecificationNotComposedException("...");
}
this.operatorsList.add(this.operatorFactory.getAndOperator());
this.specificationsList.add(specification);
return this;
}
public CompositeSpecification or(CompositeSpecification<S, O> specification) {
if (!specification.equals(this)) {
throw new IllegalArgumentException("...");
}
if (this.specificationsList.isEmpty()) {
throw new SpecificationNotComposedException("...");
}
this.operatorsList.add(this.operatorFactory.getOrOperator());
this.specificationsList.add(specification);
return this;
}
public CompositeSpecification andNot(S specification) {
if (this.specificationsList.isEmpty()) {
throw new SpecificationNotComposedException("...");
}
this.operatorsList.add(this.operatorFactory.getAndOperator());
this.specificationsList.add(new InverseSpecification(this.adaptSpecification(specification)));
return this;
}
public CompositeSpecification orNot(S specification) {
if (this.specificationsList.isEmpty()) {
throw new SpecificationNotComposedException("...");
}
this.operatorsList.add(this.operatorFactory.getOrOperator());
this.specificationsList.add(new InverseSpecification(this.adaptSpecification(specification)));
return this;
}
public CompositeSpecification andNot(CompositeSpecification<S, O> specification) {
if (!specification.equals(this)) {
throw new IllegalArgumentException("...");
}
if (this.specificationsList.isEmpty()) {
throw new SpecificationNotComposedException("...");
}
this.operatorsList.add(this.operatorFactory.getAndOperator());
this.specificationsList.add(new InverseSpecification(specification));
return this;
}
public CompositeSpecification orNot(CompositeSpecification<S, O> specification) {
if (!specification.equals(this)) {...");
}
if (this.specificationsList.isEmpty()) {
throw new SpecificationNotComposedException("...");
}
this.operatorsList.add(this.operatorFactory.getOrOperator());
this.specificationsList.add(new InverseSpecification(specification));
return this;
}
public boolean evaluate(O object) {
if (this.specificationsList.isEmpty()) {
throw new SpecificationNotComposedException("...");
}
Iterator<Specification> specificationsIt = this.specificationsList.iterator();
boolean result = specificationsIt.next().evaluate(object);
for (BinaryOperator op : operatorsList) {
result = op.evaluate(specificationsIt.next(), result, object);
}
return result;
}
public boolean equals(Object obj) {
if (obj == null) return false;
if (! (obj instanceof CompositeSpecificationImpl)) {
return false;
}
else {
CompositeSpecificationImpl otherSpec = (CompositeSpecificationImpl) obj;
EqualsBuilder builder = new EqualsBuilder();
return builder.append(this.specificationClass,
otherSpec.specificationClass)
.append(this.specificationMethod,
otherSpec.specificationMethod)
.isEquals();
}
}
public int hashCode() {
HashCodeBuilder builder = new HashCodeBuilder();
return builder.append(this.specificationClass)
.append(this.specificationMethod)
.toHashCode();
}
private Specification adaptSpecification(S specification) {
return new SpecificationGenericAdapter(specification, this.specificationMethod);
}
}
The most important thing to note is how the composite specification is internally constructed and evaluated.
Each time a new specification is composed, the new specification and the operator used for composing are stored into two different lists acting as queues:
private List<Specification> specificationsList = new LinkedList<Specification>();
private List<BinaryOperator> operatorsList = new LinkedList<BinaryOperator>();
Then, when the
evaluate() method gets called, each operator is applied to its corresponding specification, starting from the first one added:
Iterator<Specification> specificationsIt = this.specificationsList.iterator();
boolean result = specificationsIt.next().evaluate(object);
for (BinaryOperator op : operatorsList) {
result = op.evaluate(specificationsIt.next(), result, object);
}
return result;
Now, your CompositeSpecification is ready to be used:
compositeSpecification.compose(simpleSpecification1).and(simpleSpecification2).evaluate(objectToEvaluate);
Every feedback will be highly appreciated!
Updates:
09/05/2006 : Enhancements in explaining code snippets regarding logical operators.