Value
parameterization is useful - but only to certain extent. When we try to handle
additional use cases, we find the need to handle many special cases. Some
developers, try to deal with these special cases using special values (-1,
Integer.MAX_VALUE, null) However this is error prone and adds unnecessary
complexity to the code.
Lets start by looking at an example.
Say we are developing a car sales application.
The entity class may look something
like this.
public class Car {
private String make;
private String model;
private String type;
private Integer year;
private Integer kilometers;
private String colour;
private Transmission transmision;
private BigDecimal price;
//getters and setters
}
A functionality required for the app
might be the ability to display between a year range. The following method
would cater this;
void showCarsFilterByYearRange(Integer min, Integer max){
for (Car c : getAllCars()) {
if(c.getYear() > min && c.getYear() < max){
display(c);
}
}
}
We can call this with a range like
showCarsFilterByYearRange(2000, 2005);
This works fine for the range, but we
are forced to provide a max year even if we don’t want to. Maybe we can modify
the method to support null value params and treat it as a special value ( in
this case, when max is null we can safely substitute Integer.MAX_VALUE in it’s place
as we are dealing with years here )
void showCarsFilterByYearRange(Integer min, Integer max){
for (Car c : getAllCars()) {
if(c.getYear() > ((min != null)? min : 0)
&& c.getYear() < ((max != null)? max : Integer.MAX_VALUE)){
display(c);
}
}
}
Now we can only display cars after a
particular YOM by passing in null for max
showCarsFilterByYearRange(2000, null);
Sure this works, but what about
additional search requirements? We are bound to need to search vehicles by
other parameters such as price, kilometers, transmission. And surely, you
should also be able to apply multiple filters? Our approach is obviously quite
brittle and code complexity could increase exponentially with each new
requirement.
The solution? Parameterization
of behaviour (as opposed to values and types)
The expected behavior should be able
to be passed as a function...
In our
usecase we need to pass the car filtering logic as such a function. Unfortunately,
(at least before Java 8) you cannot just pass a method as a parameter - you can
only pass instances of objects. Therefore, this will need to be implemented using
functional interfaces (also known as Single Abstract Method (SAM) interfaces).
A functional
interface is an interface with only one
method. In our case, this method needs to apply a filter to a Car and
return a boolean flag.
public interface CarPredicate { boolean test(Car p); }
We can
have a single filter method that will expect an instance that implements this
interface, and uses test method to check the cars.
public void showFilteredCars(CarPredicate pred){ for(Car c: getAllCars()){ if(pred.test(c)){ display(c); } } }
On the
caller end, we can wrap an anonymous inner class declaration and instantiation
along with an implementation for the test method and pass it to our showFilteredCars().
In the example below we are displaying the cars made after 2008.
showFilteredCars(new CarPredicate(){ public boolean test(Car c){ return c.getYear() > 2008; } });
Note we’ve now
parameterized the behaviour! The filtering logic is pushed out to the caller, which
means we can do any type of filtering without having to touch showFilteredCars().
For example if we need to filter and display cars made after 2008 with manual transmission, the caller
just needs to add that logic in;
showFilteredCars(new CarPredicate(){ public boolean test(Car c){ return c.getYear() > 2008 && c.getTransmision().equals(Transmission.MANUAL); } });
It’s
instantly obvious that this is a much better implementation. However, there is just
too much boilerplate code here which deters programmers from following this
approach. That’s where Java8 Lambda expressions come in - we can define the
same logic as above with the minimum effort, only using the ‘important bits’.
Revisiting
the previous example below, I have highlighted what we can consider the ‘important
bits’. I.e (1) the parameter the predicate takes, and (2) the logic of what it
returns.
showFilteredCars(new CarPredicate(){ public boolean test(Car c){ return c.getYear() > 2008; } });
With Java 8
lambda expressions (also called closures) we only need to specify these two
things using the following syntax.
showFilteredCars(c -> c.getYear() > 2008);
It’s
important to note that internally still gets converted to an instance of a
functional interface. The compiler figures out what type c has to be through type inference.
Note that we
did not need to update our showFilteredCars() implementation. However, it turns
out that a functional interface that takes an object and returns a Boolean is
such a common case that Java8 also provides and generalized predicate so that
we don’t have to write our own.
Interface Predicate{ Boolean test(T t); }
So for
completeness, we may update our showFilteredCars() to use this instead.
public void showFilteredCars(Predicatepred){ for(Car c: getAllCars()){ if(pred.test(c)){ display(c); } } }
The sample
code for the above examples are available here
Eclipse IDE
with support for Lambda expressions can be found here
Very nice article. Learnt a lot from this. Need to try this. Thanks and Merry christmas
ReplyDeleteThanks G-man! Means a lot especially since it's my first attempt at this :)
Delete