It's about software design - IASD
/*
* IASD (It's about software design)
*/
public class FilterTest
{
@Test
public void findByName() throws Exception
{
Person john = new Person("John", 20);
Person markos = new Person("Markos", 30);
Condition<Person> isNamedJohn = Conditions.hasName("John");
Person actual = Filters.of(john, markos).find( isNamedJohn );
Assert.assertEquals(john, actual);
}
@Test
public void findByAge() throws Exception
{
Person john = new Person("John", 20);
Person markos = new Person("Markos", 30);
Condition<Integer> between17and25 =
new IntegerConditionChain()
.gt(17)
.lt(25);
Condition<Person> agedBetween17and25 =
Conditions.age( between17and25 );
Person actual = Filters
.of(john, markos)
.find( agedBetween17and25 );
Assert.assertEquals(john, actual);
}
@Test
public void findByNameAndAge() throws Exception
{
Person johnAged20 = new Person("John", 20);
Person johnAged21 = new Person("John", 21);
Person johnAged29 = new Person("John", 29);
Person johnAged30 = new Person("John", 30);
Person markosAged30 = new Person("Markos", 29);
Condition<Integer> between20and30 =
new IntegerConditionChain()
.gt(20)
.lt(30);
Condition<Person> namedJohnBetween20And30 =
new PersonConditionChain()
.hasName("John")
.age(between20and30);
List<Person> actual = Filters
.of(johnAged20,
johnAged21,
johnAged29,
johnAged30,
markosAged30)
.list( namedJohnBetween20And30 );
Assert.assertEquals(2, actual.size());
Assert.assertEquals(
Lists.newArrayList(johnAged21, johnAged29),
actual);
}
}
Find it amazing how a single method interface can be the inspiration to a good software design.
Here is the culprit.
public interface Condition<T>
{
public boolean apply(T type);
}
The idea is that you can apply a condition to any type and see if it adheres to that.
Notice how the usage of generics elevates the design. Using an Object instead would have made the code vulnerable to ClassCastException and fragile to piece together with other classes. An interface instead would force client code to implement it, pose a limit on the conditions you can apply and is rather stiff.
Then came the Filter allowing you to extract the types that satisfy the conditions of yours.
public interface Filter<T>
{
public T find(Condition<T> condition);
public List<T> list(Condition<T> condition);
}
Then the requirement to apply a list of Conditions.
public interface ConditionChain<T> extends Condition<T>
{
public boolean apply(T type);
public ConditionChain<T> addCondition(Condition<T> condition);
}
Without differentiating from the Condition interface thus being able to accomodate for it in the existing API.
With implementations providing public methods to specific conditions that make sense on the type
public final class IntegerConditionChain implements ConditionChain<Integer>
{
public IntegerConditionChain gt(final int that)
{
this.addCondition(new Condition<Integer>(){
@Override
public boolean apply(Integer value) {
return value > that;
}
});
return this;
}
public IntegerConditionChain lt(final int that)
{
this.addCondition(new Condition<Integer>(){
@Override
public boolean apply(Integer value) {
return value < that;
}
});
return this;
}
}
It’s your turn now to DRY the design by introducing a class that encapsulates the implementation of the methods related to the ConditionChain thus increasing cohesion and lowering coupling for creating specific conditions without abusing inheritance.
While doing so you get to win a postcard from Edinburgh.