Specifications are template-like objects for powerfully expressing expected state and behaviour of an object. Examples of usage are selection/projection, validation, partitioning, and creation of objects. Every specification has a boolean method for verifying if a candidate object satisfies the specification rules.
The Specifications pattern is already fully explained in the excellent original article. Go read it, then continue exploring Domian.
So, do you find expressions like these appealing?
Specification<Customer> specialTreatmentSpec = shouldBe(femaleCustomer).or(both(maleCustomer, vipCustomer));
If so, read on!
All Domian specifications implement the net.sourceforge.domian.specification.Specification interface.
public interface Specification<T> { Class<T> getType(); Boolean isSatisfiedBy (T candidate); Boolean isGeneralizationOf (Specification<? extends T> otherSpecification); Boolean isSpecialCaseOf (Specification<? super T> otherSpecification); Boolean isDisjointWith (Specification<?> otherSpecification); }
See the net.sourceforge.domian.specification.Specification Javadoc.
A specification defines an object space. All objects residing in such a specification-defined space is satisfied by that specification. Other Specification methods describes set relations between specifications. Figure 1 shows four specifications and their set relations.
Figure 1 - Venn diagram of object space sets defined by specifications
Table 1 - 4 shows the "truth table" for specification A, specification B, specification C, and specification D. Their relations are illustrated in Figure 1.
A | isGeneralizationOf(...) | isSpecialCaseOf(...) | isDisjointWith(...) |
A | true | true | false |
B | false | false | false |
C | true | false | false |
D | true | false | false |
B | isGeneralizationOf(...) | isSpecialCaseOf(...) | isDisjointWith(...) |
A | false | false | false |
B | true | true | false |
C | false | false | false |
D | false | false | true |
C | isGeneralizationOf(...) | isSpecialCaseOf(...) | isDisjointWith(...) |
A | false | true | false |
B | false | false | false |
C | true | true | false |
D | false | false | false |
D | isGeneralizationOf(...) | isSpecialCaseOf(...) | isDisjointWith(...) |
A | false | true | false |
B | false | false | true |
C | false | false | false |
D | true | true | false |
Domian specifications can only be created by the net.sourceforge.domian.specification.SpecificationFactory class. The SpecificationFactory class is a collection of static factory methods for creating Specification objects. The methods in the SpecificationFactory class are heavily aliased promoting humane interfaces, with the noble goal of achieving some fluent expressions. See the SpecificationFactory Javadoc.
All code examples below can be found here.
We will be using this vanilla CRM domain model when explaining Domian usage. I guess you have seen it before...
This statement creates a specification that approves all kind of objects:
net.sourceforge.domian.specification.Specification spec = net.sourceforge.domian.specification.SpecificationFactory.allObjects();
Not very useful, but a natural starting point... The statement should be made more readable using regular and static imports. (It is really recommended using an IDE that supports automatic generation of import statements.)
Specification spec = allObjects();
These statements are now true:
assertFalse(spec.isSatisfiedBy(null)); // null references are never approved by Domian specifications assertTrue(spec.isSatisfiedBy("")); assertTrue(spec.isSatisfiedBy(new Float("3.14"))); assertTrue(spec.isSatisfiedBy(new Date()));
From now on, regular Java imports are applied in all code examples.
This statement creates a specification that approves all Customer objects:
Specification<Customer> spec = SpecificationFactory.createSpecificationFor(Customer.class);
Again, we are making the statement more readable using a static import of the createSpecificationFor method:
Specification<Customer> spec = createSpecificationFor(Customer.class);
These statements are now true:
assertTrue(spec.isSatisfiedBy(aCustomer)); assertTrue(spec.isSatisfiedBy(anotherCustomer)); assertTrue(spec.isSatisfiedBy(aSubclassedCustomer)); //assertFalse(spec.isSatisfiedBy(new Date())); // does not compile
Any attempts to approve an object of another type than Customer with this specification, will not compile due to type parameterization. If you declare raw specifications, they will approve candidate objects of wrong type at compile-time, while at run-time an exception will be thrown - an informative one that is.
Types specifications like these are not very useful on their own, but they represent the most common starting point for creating complex specifications.
From now on, static imports of SpecificationFactory methods are applied in all example code.
Most complex classes contains members of types like strings of characters, numbers, dates, other complex types, collections, and maps. Domian includes specifications for these types, and SpecificationFactory has factory methods for all of them.
Equality is covered by EqualSpecification, and EqualIgnoreCaseStringSpecification. EqualSpecification is the generic one, taking all possible types, and uses the equals() method for testing equality. EqualIgnoreCaseStringSpecification handles, well, strings where case is ignored. (it uses the special equalIgnoreCase() method in the String class)
EqualSpecification object are available via the SpecificationFactory method createEqualSpecification, or the alias methods isEqualTo, equalTo, exactly, and is. Also, boolean aliases isTrue and isFalse is included. For Dates, the equality factory method is named isAtTheSameTimeAs, atTheSameTimeAs, and at.
Specification<Long> long42 = equalTo(42L); Specification<Date> oneYearAgo = atTheSameTimeAs(createOneYearAgoDate());
(createOneYearAgoDate() is an internal static helper method, returning a java.util.Date instance.)
The other class of specifications dealing with equality is based on regular expressions. RegularExpressionMatcherStringSpecification object are available via the SpecificationFactory method createRegularExpressionMatcherStringSpecification, or the alias methods matchesRegularExpression, matchesRegex, and matches. Here is a simple RegularExpressionMatcherStringSpecification test example:
Specification<String> shorterThanThree = matchesRegularExpression(".{0," + (2) + "}"); assertTrue(shorterThanThree.isSatisfiedBy("")); assertTrue(shorterThanThree.isSatisfiedBy("1")); assertTrue(shorterThanThree.isSatisfiedBy("12")); assertFalse(shorterThanThree.isSatisfiedBy("123"));
A less complicated way than using regular expressions is the use of wilcard characters. WildcardExpressionMatcherStringSpecification objects are available via the SpecificationFactory method createWildcardExpressionMatcherStringSpecification, or the alias methods matchesWildcardExpression, and like (inspired by the SQL operator). Here is a simple WildcardExpressionMatcherStringSpecification test example:
Specification<String> startsAndEndsWithA_AtLeastSevenCharsLong = matchesWildcardExpression("A?????*a"); assertFalse(startsAndEndsWithA_AtLeastSevenCharsLong.isSatisfiedBy("Aloha")); assertTrue(startsAndEndsWithA_AtLeastSevenCharsLong.isSatisfiedBy("Arizona")); assertTrue(startsAndEndsWithA_AtLeastSevenCharsLong.isSatisfiedBy("Australia"));
Specifications for less than, less than or equal, greater than, and greater than or equal can be created for all java.lang.Comparable objects. The SpecificationFactory methods creating comparison specifications have names like e.g.: isLessThan, isLessThanOrEqual/atMost, isGreaterThan/moreThan, and isGreaterThanOrEqual/atLeast. For date values, they are called isBefore, isBeforeOrAtTheSameTimeAs, isAfter, and isAfterOrAtTheSameTimeAs.
Specification<Integer> greaterThan10 = isGreaterThan(10); Specification<Date> lessThanOneYearAgo = isAfter(createOneYearAgoDate());
DefaultValueSpecification approves default object values, e.g. zero Numbers, and blank Strings. Maybe more interesting is that it recognized the opposite, shown below. DefaultValueSpecification object are available via the SpecificationFactory methods createBlankStringSpecification/blankString, and createDefaultNumberSpecification/defaultNumber.
Specification<Number> defaultNumber = createDefaultNumberSpecification(); assertTrue(defaultNumber.isSatisfiedBy(0.0)); assertFalse(defaultNumber.isSatisfiedBy(0.1)); Specification<Number> notDefaultNumber = not(defaultNumber); assertFalse(notDefaultNumber.isSatisfiedBy(0.0)); assertTrue(notDefaultNumber.isSatisfiedBy(0.1));
DateStringSpecification approves date strings formatted in a pre-defined way, representing a valid date. DateStringSpecification object are available via the SpecificationFactory method createDateStringSpecification, or the alias isDate. Here is a DateStringSpecification test example:
Specification<String> spec = isDate("ddMMyyyy"); assertFalse(spec.isSatisfiedBy("01-01-2007")); // Not correct date format assertFalse(spec.isSatisfiedBy("32032007")); // Illegal date assertFalse(spec.isSatisfiedBy("01132007")); // Illegal month assertFalse(spec.isSatisfiedBy("29022007")); // Not a leap year assertTrue(spec.isSatisfiedBy("01012007"));
Also, a convenience methods for creating date comparison specifications via a DateStringSpecification exists, supporting a relevant set of date formats, e.g.:
Specification<Date> dateSpec = before("2008-06-07"); assertFalse(dateSpec.isSatisfiedBy(getTime(2008, 6, 7))); assertTrue(dateSpec.isSatisfiedBy(getTime(2008, 6, 6)));
EnumNameStringSpecification approves only strings that is a declared name in a given java.lang.Enum class. EnumNameStringSpecification object are available via the SpecificationFactory method createEnumNameStringSpecification, or the alias isEnum. Here is a simple EnumNameStringSpecification test example:
enum Colour { BLACK, WHITE } Specification<String> spec = isEnum(Colour.class); assertFalse(spec.isSatisfiedBy("01012007")); assertFalse(spec.isSatisfiedBy("SHADY")); assertTrue(spec.isSatisfiedBy("WHITE"));
As pointed out in the Evans/Fowler article it is cumbersome to create hard-coded specifications for all sort of instance state permutations. It is more flexible with parameterized specifications, but may still be cumbersome when you have to create them individually for all object types. Domian includes a generic version of a parameterized specification. The class ParameterizedSpecification uses java.lang.reflect.AccessibleObject objects directly; and together with a Specification, the accessible object is specified as wanted.
Parameterized specification is easiest recognized by the need for two parameters. When creating a parameterized specification one needs one parameter for the field/method name, and another one for the field/method specification, e.g.:
Specification<Customer> femaleCustomers = all(Customer.class).where("gender", is(FEMALE));
Several of these parameterized specifications can be combined into composite specifications, creating a complete specification of an overall class.
Composite specifications combines other specifications. All types of specifications can be combined; leaf specifications, negated specifications and/or other composite specifications. They are combined using logical operator methods, like AND (conjunction), OR (disjunction), and NOT (negation).
Here a date span is specified using two java.util.Date specifications and a conjunction:
Specification<Date> lessThanThirtyYearsAgo = isAfter(createThirtyYearsAgoDate()); Specification<Date> lessThanThirtyYearsAhead = isBefore(createThirtyYearsAheadDate()); Specification<Date> plusMinusThirtyYears = is(lessThanThirtyYearsAgo).and(lessThanThirtyYearsAhead); assertTrue(plusMinusThirtyYears.isSatisfiedBy(new Date())); assertTrue(plusMinusThirtyYears.isSatisfiedBy(getTime(1990, 7, 30))); assertTrue(plusMinusThirtyYears.isSatisfiedBy(getTime(2037, 7, 29))); assertFalse(plusMinusThirtyYears.isSatisfiedBy(getTime(1977, 7, 30))); assertFalse(plusMinusThirtyYears.isSatisfiedBy(getTime(2050, 7, 30)));
(getTime() is an internal static helper method, returning a java.util.Date instance.)
The same specification could be created this way, using the vararg factory method: public static <T> CompositeSpecification<T> both(final Specification<T>... specifications):
plusMinusThirtyYears = both(lessThanThirtyYearsAgo, lessThanThirtyYearsAhead);
The next code example shows a composite specifications built by four parameterized specifications, chained with logical operators. It specifies all Customers who is older than ten years. In addition, the Customers should be male. If the Customers is female, she must have been member for more than a year!
Specification<Customer> spec = specify(Customer.class) .where("gender", is(FEMALE)) .and("membershipDate", isBefore(oneYearAgo)) .or("gender", is(MALE)) .and("birthDate", is(not(afterOrAtTheSameTimeAs(tenYearsAgo))));
Building Domian specifications is not an associative routine. All logical operators (methods in the CompositeSpecification interface) takes the specification on its left side as one operand, and the specification on its right side as the other operand. So, the order of the specification components do counts when chaining together a composite specification.
Below the specification from the above example is built in another fashion. It is assembled using four Customer specification components. Parenthesis are added showing the resulting order in which the four component specifications are put together.
Specification<Customer> femaleCustomer = specify(Customer.class).where("gender", is(FEMALE)); Specification<Customer> maleCustomer = specify(Customer.class).where("gender", is(MALE)); Specification<Customer> veteranCustomer = specify(Customer.class).where("membershipDate", isBefore(oneYearAgo)); Specification<Customer> childCustomer = specify(Customer.class).where("birthDate", isAfterOrAtTheSameTimeAs(tenYearsAgo)); spec = ((both(femaleCustomer, veteranCustomer)).or(a(maleCustomer))).and(not(a(childCustomer)));
Without unnecessary paranthesis (and some unnecessary 'a' specification wrappers) it looks like this:
spec = both(femaleCustomer, veteranCustomer).or(maleCustomer).and(not(childCustomer));
Applying some boolean algebra we end up with this equivalent specification expression:
spec = a(veteranCustomer).or(maleCustomer).and(not(childCustomer));
...which is not the same as:
spec = a(maleCustomer).and(not(childCustomer)).or(veteranCustomer);
So, to create Domian specifications you use factory methods in SpecificationFactory to create standalone specifications. Then you use the logical operator methods in CompositeSpecification to combine them, and chain them.
The CollectionSpecification class allow us to include java.util.Collection types into our specifications, making it possible to create more expressive, yet compact specifications.
The CollectionSpecification class has three scopes as listed in Table 2.
# | Scope | Factory method aliases |
1 | The number of collection elements (collection size) | hasSize, haveSize, hasSizeOf, haveSizeOf, isEmpty, empty |
2 | The number of collection elements satisfying a given element specification | include, includes |
3 | The percentage of collection elements satisfying a given element specification | includeAPercentageOf, includesAPercentageOf |
Now, we will use each of these three scopes when we declare what a good customer should mean in our fictitious domain.
Here is a simple declaration of good customers; they just have to have at least four orders in total.
Specification<Customer> goodCustomers = all(Customer.class).where("orders", haveSizeOf(atLeast(4)));
Just counting the number of Order instances hides a lot of behaviour for our Customer objects.
Here is an upgraded declaration of good customers; they have to have orders from the three last years; and not only one order, but at least two orders, for each of the last three calendar years.
First, we create leaf specifications of the dates we need:
Specification<Date> afterThisYear = isAfterOrAtTheSameTimeAs(beginningOfNextYear); Specification<Date> beforeThisYear = isBefore(beginningOfThisYear); Specification<Date> beforeLastYear = isBefore(beginningOfLastYear); Specification<Date> beforeTheYearBeforeLastYear = isBefore(beginningOfTwoYearsAgo);
Then we specify date spans with some simple composite specifications:
Specification<Date> duringThisYear = is(not(beforeThisYear)).and(not(afterThisYear)); Specification<Date> duringLastYear = is(not(beforeLastYear)).and(beforeThisYear); Specification<Date> duringTheYearBeforeLastYear = is(not(beforeTheYearBeforeLastYear)).and(beforeLastYear);
Now we need to specify Orders from each of the three last years.
Specification<Order> ordersFromThisYear = all(Order.class).where("orderDate", isFrom(duringThisYear)); Specification<Order> ordersFromLastYear = all(Order.class).where("orderDate", isFrom(duringLastYear)); Specification<Order> ordersFromTheYearBeforeLastYear = all(Order.class).where("orderDate", isFrom(duringTheYearBeforeLastYear));
Now we have what we need. All specifications in the previous three code blocks are completely generic, immutable, and reusable. They should be made static, and moved to a common class for everybody to use.
These are the few lines of business logic which are included in our domain class:
CompositeSpecification<Customer> goodCustomers = all(Customer.class) .where("orders", include(atLeast(2), ordersFromThisYear)) .and("orders", include(atLeast(2), ordersFromLastYear)) .and("orders", include(atLeast(2), ordersFromTheYearBeforeLastYear));
Voila, this is a language even business people understand ;-)
Well, we are still just counting the number of Order instances for our Customer objects... Let us add some demands for the Orders to be taken into consideration.
Like before, we need some underlying specifications:
Specification<OrderLine> cancelledOrderLines = all(OrderLine.class).where("cancelled", isTrue()); Specification<OrderLine> pendingOrderLines = all(OrderLine.class).where("paymentReceived", isFalse());
Now, we declare bad orders to be orders where at least 25% of the order lines are cancelled. We also declare pending orders to be more than one month old orders, where at least 50% of the order lines are not paid for yet.
Specification<Order> badOrders = all(Order.class).where("orderLines", includeAPercentageOf(atLeast(25), cancelledOrderLines)); Specification<Order> pendingOrders = all(Order.class).where("orderDate", isBefore(aMonthAgo)).and("orderLines", includeAPercentageOf(atLeast(50), pendingOrderLines));
Let us say a bad customer is a customer having a total collection of orders of which more than 20% are bad orders, or more than 10% are pending orders.
Specification<Customer> badCustomers = all(Customer.class) .where("orders", includeAPercentageOf(moreThan(20), badOrders)) .or("orders", includeAPercentageOf(moreThan(10), pendingOrders));
Now, we can re-declare our good customers to be both good, and not bad for business.
goodCustomers = goodCustomers.and(not(badCustomers));
The order in which the above specifications are declared, is optimized for readability. When it comes to development, the most natural order is perhaps the other way around; starting with the customer specifications, and then drilling down recursively as long as one feel necessary; checking for possible re-use as you go along.