Skip to content

Commit 73f9b8b

Browse files
MaVdbusscheiluwatar
authored andcommitted
Adding composite specification (Issue#1093) (iluwatar#1094)
* Resolution proposition to Issue#1055 (UML diagram left to do) * Deciding not to modify the UML diagram for now * Resolution proposition to Issue#1093 * Code reformatting
1 parent 19b129c commit 73f9b8b

20 files changed

+492
-73
lines changed

specification/README.md

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ Use the Specification pattern when
3131

3232
Real world example
3333

34-
> There is a pool of different creatures and we often need to select some subset of them. We can write our search specification such as "creatures that can fly" or "creatures heavier than 500 kilograms" and give it to the party that will perform the filtering.
34+
> There is a pool of different creatures and we often need to select some subset of them.
35+
> We can write our search specification such as "creatures that can fly", "creatures heavier than 500 kilograms", or as a combination of other search specifications, and then give it to the party that will perform the filtering.
3536
3637
In Plain Words
3738

@@ -44,8 +45,10 @@ Wikipedia says
4445
**Programmatic Example**
4546

4647
If we look at our creature pool example from above, we have a set of creatures with certain properties.\
47-
Those properties can be part of a pre-defined, limited set (represented here by the enums Size, Movement and Color); but they can also be discrete (e.g. the mass of a Creature). In this case, it is more appropriate to use what we call "parameterized specification", where the property value can be given as an argument when the Creature is created, allowing for more flexibility.
48-
48+
Those properties can be part of a pre-defined, limited set (represented here by the enums Size, Movement and Color); but they can also be continuous values (e.g. the mass of a Creature).
49+
In this case, it is more appropriate to use what we call "parameterized specification", where the property value can be given as an argument when the Creature is instantiated, allowing for more flexibility.
50+
A third option is to combine pre-defined and/or parameterized properties using boolean logic, allowing for near-endless selection possibilities (this is called Composite Specification, see below).
51+
The pros and cons of each approach are detailed in the table at the end of this document.
4952
```java
5053
public interface Creature {
5154
String getName();
@@ -56,8 +59,7 @@ public interface Creature {
5659
}
5760
```
5861

59-
And dragon implementation looks like this.
60-
62+
And ``Dragon`` implementation looks like this.
6163
```java
6264
public class Dragon extends AbstractCreature {
6365

@@ -67,10 +69,9 @@ public class Dragon extends AbstractCreature {
6769
}
6870
```
6971

70-
Now that we want to select some subset of them, we use selectors. To select creatures that fly, we should use MovementSelector.
71-
72+
Now that we want to select some subset of them, we use selectors. To select creatures that fly, we should use ``MovementSelector``.
7273
```java
73-
public class MovementSelector implements Predicate<Creature> {
74+
public class MovementSelector extends AbstractSelector<Creature> {
7475

7576
private final Movement movement;
7677

@@ -85,10 +86,9 @@ public class MovementSelector implements Predicate<Creature> {
8586
}
8687
```
8788

88-
On the other hand, we selecting creatures heavier than a chosen amount, we use MassGreaterThanSelector.
89-
89+
On the other hand, when selecting creatures heavier than a chosen amount, we use ``MassGreaterThanSelector``.
9090
```java
91-
public class MassGreaterThanSelector implements Predicate<Creature> {
91+
public class MassGreaterThanSelector extends AbstractSelector<Creature> {
9292

9393
private final Mass mass;
9494

@@ -103,20 +103,84 @@ public class MassGreaterThanSelector implements Predicate<Creature> {
103103
}
104104
```
105105

106-
With these building blocks in place, we can perform a search for red and flying creatures like this.
106+
With these building blocks in place, we can perform a search for red creatures as follows :
107+
```java
108+
List<Creature> redCreatures = creatures.stream().filter(new ColorSelector(Color.RED))
109+
.collect(Collectors.toList());
110+
```
107111

112+
But we could also use our parameterized selector like this :
108113
```java
109-
List<Creature> redAndFlyingCreatures = creatures.stream()
110-
.filter(new ColorSelector(Color.RED).and(new MovementSelector(Movement.FLYING))).collect(Collectors.toList());
114+
List<Creature> heavyCreatures = creatures.stream().filter(new MassGreaterThanSelector(500.0)
115+
.collect(Collectors.toList());
111116
```
112117

113-
But we could also use our paramterized selector like this.
118+
Our third option is to combine multiple selectors together. Performing a search for special creatures (defined as red, flying, and not small) could be done as follows :
119+
```java
120+
AbstractSelector specialCreaturesSelector =
121+
new ColorSelector(Color.RED).and(new MovementSelector(Movement.FLYING)).and(new SizeSelector(Size.SMALL).not());
122+
123+
List<Creature> specialCreatures = creatures.stream().filter(specialCreaturesSelector)
124+
.collect(Collectors.toList());
125+
```
126+
127+
**More on Composite Specification**
128+
129+
In Composite Specification, we will create custom instances of ``AbstractSelector`` by combining other selectors (called "leaves") using the three basic logical operators.
130+
These are implemented in ``ConjunctionSelector``, ``DisjunctionSelector`` and ``NegationSelector``.
131+
```java
132+
public abstract class AbstractSelector<T> implements Predicate<T> {
133+
134+
public AbstractSelector<T> and(AbstractSelector<T> other) {
135+
return new ConjunctionSelector<>(this, other);
136+
}
137+
138+
public AbstractSelector<T> or(AbstractSelector<T> other) {
139+
return new DisjunctionSelector<>(this, other);
140+
}
114141

142+
public AbstractSelector<T> not() {
143+
return new NegationSelector<>(this);
144+
}
145+
}
146+
```
115147
```java
116-
List<Creature> heavyCreatures = creatures.stream()
117-
.filter(new MassGreaterThanSelector(500.0).collect(Collectors.toList());
148+
public class ConjunctionSelector<T> extends AbstractSelector<T> {
149+
150+
private List<AbstractSelector<T>> leafComponents;
151+
152+
@SafeVarargs
153+
ConjunctionSelector(AbstractSelector<T>... selectors) {
154+
this.leafComponents = List.of(selectors);
155+
}
156+
157+
/**
158+
* Tests if *all* selectors pass the test.
159+
*/
160+
@Override
161+
public boolean test(T t) {
162+
return leafComponents.stream().allMatch(comp -> (comp.test(t)));
163+
}
164+
}
118165
```
119166

167+
All that is left to do is now to create leaf selectors (be it hard-coded or parameterized ones) that are as generic as possible,
168+
and we will be able to instantiate the ``AbstractSelector`` class by combining any amount of selectors, as exemplified above.
169+
We should be careful though, as it is easy to make a mistake when combining many logical operators; in particular, we should pay attention to the priority of the operations.\
170+
In general, Composite Specification is a great way to write more reusable code, as there is no need to create a Selector class for each filtering operation.
171+
Instead, we just create an instance of ``AbstractSelector`` "on the spot", using tour generic "leaf" selectors and some basic boolean logic.
172+
173+
174+
**Comparison of the different approaches**
175+
176+
| Pattern | Usage | Pros | Cons |
177+
|---|---|---|---|
178+
| Hard-Coded Specification | Selection criteria are few and known in advance | + Easy to implement | - Inflexible |
179+
| | | + Expressive |
180+
| Parameterized Specification | Selection criteria are a large range of values (e.g. mass, speed,...) | + Some flexibility | - Still requires special-purpose classes |
181+
| Composite Specification | There are a lot of selection criteria that can be combined in multiple ways, hence it is not feasible to create a class for each selector | + Very flexible, without requiring many specialized classes | - Somewhat more difficult to comprehend |
182+
| | | + Supports logical operations | - You still need to create the base classes used as leaves |
183+
120184
## Related patterns
121185

122186
* Repository

specification/src/main/java/com/iluwatar/specification/app/App.java

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@
3131
import com.iluwatar.specification.creature.Shark;
3232
import com.iluwatar.specification.creature.Troll;
3333
import com.iluwatar.specification.property.Color;
34-
import com.iluwatar.specification.property.Mass;
3534
import com.iluwatar.specification.property.Movement;
35+
import com.iluwatar.specification.selector.AbstractSelector;
3636
import com.iluwatar.specification.selector.ColorSelector;
37+
import com.iluwatar.specification.selector.MassEqualSelector;
3738
import com.iluwatar.specification.selector.MassGreaterThanSelector;
3839
import com.iluwatar.specification.selector.MassSmallerThanOrEqSelector;
3940
import com.iluwatar.specification.selector.MovementSelector;
@@ -77,13 +78,8 @@ public static void main(String[] args) {
7778
List<Creature> darkCreatures =
7879
creatures.stream().filter(new ColorSelector(Color.DARK)).collect(Collectors.toList());
7980
darkCreatures.forEach(c -> LOGGER.info(c.toString()));
80-
// find all red and flying creatures
81-
LOGGER.info("Find all red and flying creatures");
82-
List<Creature> redAndFlyingCreatures =
83-
creatures.stream()
84-
.filter(new ColorSelector(Color.RED).and(new MovementSelector(Movement.FLYING)))
85-
.collect(Collectors.toList());
86-
redAndFlyingCreatures.forEach(c -> LOGGER.info(c.toString()));
81+
82+
LOGGER.info("\n");
8783
// so-called "parameterized" specification
8884
LOGGER.info("Demonstrating parameterized specification :");
8985
// find all creatures heavier than 500kg
@@ -98,5 +94,26 @@ public static void main(String[] args) {
9894
creatures.stream().filter(new MassSmallerThanOrEqSelector(500.0))
9995
.collect(Collectors.toList());
10096
lightCreatures.forEach(c -> LOGGER.info(c.toString()));
97+
98+
LOGGER.info("\n");
99+
// so-called "composite" specification
100+
LOGGER.info("Demonstrating composite specification :");
101+
// find all red and flying creatures
102+
LOGGER.info("Find all red and flying creatures");
103+
List<Creature> redAndFlyingCreatures =
104+
creatures.stream()
105+
.filter(new ColorSelector(Color.RED).and(new MovementSelector(Movement.FLYING)))
106+
.collect(Collectors.toList());
107+
redAndFlyingCreatures.forEach(c -> LOGGER.info(c.toString()));
108+
// find all creatures dark or red, non-swimming, and heavier than or equal to 400kg
109+
LOGGER.info("Find all scary creatures");
110+
AbstractSelector<Creature> scaryCreaturesSelector = new ColorSelector(Color.DARK)
111+
.or(new ColorSelector(Color.RED)).and(new MovementSelector(Movement.SWIMMING).not())
112+
.and(new MassGreaterThanSelector(400.0).or(new MassEqualSelector(400.0)));
113+
List<Creature> scaryCreatures =
114+
creatures.stream()
115+
.filter(scaryCreaturesSelector)
116+
.collect(Collectors.toList());
117+
scaryCreatures.forEach(c -> LOGGER.info(c.toString()));
101118
}
102119
}

specification/src/main/java/com/iluwatar/specification/property/Mass.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323

2424
package com.iluwatar.specification.property;
2525

26-
/** Mass property. */
26+
/**
27+
* Mass property.
28+
*/
2729
public class Mass {
2830

2931
private double value;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* The MIT License
3+
* Copyright © 2014-2019 Ilkka Seppälä
4+
*
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in
13+
* all copies or substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
* THE SOFTWARE.
22+
*/
23+
24+
package com.iluwatar.specification.selector;
25+
26+
import java.util.function.Predicate;
27+
28+
/**
29+
* Base class for selectors.
30+
*/
31+
public abstract class AbstractSelector<T> implements Predicate<T> {
32+
33+
public AbstractSelector<T> and(AbstractSelector<T> other) {
34+
return new ConjunctionSelector<>(this, other);
35+
}
36+
37+
public AbstractSelector<T> or(AbstractSelector<T> other) {
38+
return new DisjunctionSelector<>(this, other);
39+
}
40+
41+
public AbstractSelector<T> not() {
42+
return new NegationSelector<>(this);
43+
}
44+
}

specification/src/main/java/com/iluwatar/specification/selector/ColorSelector.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,11 @@
2525

2626
import com.iluwatar.specification.creature.Creature;
2727
import com.iluwatar.specification.property.Color;
28-
import java.util.function.Predicate;
2928

3029
/**
3130
* Color selector.
3231
*/
33-
public class ColorSelector implements Predicate<Creature> {
32+
public class ColorSelector extends AbstractSelector<Creature> {
3433

3534
private final Color color;
3635

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* The MIT License
3+
* Copyright © 2014-2019 Ilkka Seppälä
4+
*
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in
13+
* all copies or substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
* THE SOFTWARE.
22+
*/
23+
24+
package com.iluwatar.specification.selector;
25+
26+
import java.util.List;
27+
28+
/**
29+
* A Selector defined as the conjunction (AND) of other (leaf) selectors.
30+
*/
31+
public class ConjunctionSelector<T> extends AbstractSelector<T> {
32+
33+
private List<AbstractSelector<T>> leafComponents;
34+
35+
@SafeVarargs
36+
ConjunctionSelector(AbstractSelector<T>... selectors) {
37+
this.leafComponents = List.of(selectors);
38+
}
39+
40+
/**
41+
* Tests if *all* selectors pass the test.
42+
*/
43+
@Override
44+
public boolean test(T t) {
45+
return leafComponents.stream().allMatch(comp -> (comp.test(t)));
46+
}
47+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* The MIT License
3+
* Copyright © 2014-2019 Ilkka Seppälä
4+
*
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in
13+
* all copies or substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
* THE SOFTWARE.
22+
*/
23+
24+
package com.iluwatar.specification.selector;
25+
26+
import java.util.List;
27+
28+
/**
29+
* A Selector defined as the disjunction (OR) of other (leaf) selectors.
30+
*/
31+
public class DisjunctionSelector<T> extends AbstractSelector<T> {
32+
33+
private List<AbstractSelector<T>> leafComponents;
34+
35+
@SafeVarargs
36+
DisjunctionSelector(AbstractSelector<T>... selectors) {
37+
this.leafComponents = List.of(selectors);
38+
}
39+
40+
/**
41+
* Tests if *at least one* selector passes the test.
42+
*/
43+
@Override
44+
public boolean test(T t) {
45+
return leafComponents.stream().anyMatch(comp -> comp.test(t));
46+
}
47+
}

0 commit comments

Comments
 (0)