3. First analysis - from naive to flexible
In this section we will develop traditional solutions to various filtering requirements. We avoid using Lambdas or Streams so that the techniques illustrated here can be compared with the solutions developed in Chapter 4.
3.1 Fix filter
The first task is to list all customers who are less than 20 years old. This can be done very easily; all we need is a loop with a filter condition to select young customers and a target list to collect them.
private List<Person> getPersonsLessThan20Years(List<Person> persons){
List<Person> result = new ArrayList<>();
for (Person person : persons) {
if (person.getAge() < 20) {
result.add(person);
}
}
return result;
}
3.2 Simple parameterization
The next requirement is to collect the group of people between 30 and 40 years old. Of course, we realize that in the future we may need to query different age groups and so it would be better to parameterize the method rather than hard coding the condition.
private List<Person> getPersonsByAgeRange(
List<Person> persons,
int from,
int to) {
List<Person> result = new ArrayList<>();
for (Person person : persons) {
if (person.getAge() >= from && person.getAge() <= to) {
result.add(person);
}
}
return result;
}
Here, the developer has introduced some flexibility. However, this method still does no more than select persons of a specified age group. If additional criteria are needed, like querying the gender, this method doesn’t help. A novice programmer might try to solve the problem by adding extra parameters for gender, vendor status etc.
private List<Person> getPersonsByDiverseCriteria(
List<Person> persons,
int ageFrom,
int ageTo,
Gender gender,
boolean isCustomer,
boolean isVendor) {
[loop omitted]
}
Senior developers might shake their heads at such naive code; their experience tells them that one day you won’t be able to do your analysis because you will need at least one more parameter.
3.3 Behavior parameterization
The next evolutionary step towards a better solution is to create the condition or filter as a stand-alone object and to pass it to the now more general purpose method. This allows the method to be parameterized with different behavior, or different algorithms, reminding us of the strategy pattern.
For our task this behavior will implement an interface which contains a test to choose a person by a specified condition. Let’s call this interface Condition.
public interface Condition<T> {
boolean test(T t);
}
Now our method (the loop) needs only two parameters, the list of persons and the condition.
private List<Person> getPersonsByCondition(List<Person> persons, \
Condition<Person> condition){
List<Person> result = new ArrayList<>();
for (Person person : persons) {
if (condition.test(person)) {
result.add(person);
}
}
return result;
}
The condition is swapped out and will be injected by a parameter. Thus, there is no need to change the implementation of the method when we need a different filter. Now let’s refactor our first analysis to get everyone less than 20 years old.
class YoungerThanCondition implements Condition<Person>{
private final int _age;
YoungerThanCondition(int age){
_age = age;
}
@Override
public boolean test(Person person) {
return person.getAge() < _age;
}
}
Now, the code to call our loop and to inject the filter looks more clean and concise.
persons = getPersonsByCondition(persons, new YoungerThanCondition(20));
Following the object oriented paradigm we pass the condition to the method as an object. Now, if we need other filter criteria, we simply create different filter classes as implementations of the Condition interface. The loop to collect the persons of interest remains unchanged.
3.4 Anonymous classes
But creating a separate class for each different condition still seems to be a heavyweight approach. The question is: “If we only need to use the condition in one place can we create the class just where we’ll need it?” This is where anonymous classes come into play.
persons = getPersonsByCondition(persons, new Condition<Person>(){
@Override
public boolean test(Person person) {
return person.getAge() < 20;
}
});
Since the anonymous class is just created where it is needed, we can’t re-use it. It doesn’t make sense to pass the age as parameter, we simply write it directly into the condition.
Compared with the fully-fledged filter classes, anonymous classes are much shorter. But, instead of passing a short class name as parameter, we have to override the test method and to write a couple of lines. Anonymous classes are shorter than fully-fledged classes, but move the code into the parameter; this may not seem ideal. And by the way - lots of programmers dislike anonymous classes.