Java 8’s default methods on interfaces means we can implement the decorator pattern much less verbosely.
The decorator pattern allows us to add behaviour to an object without using inheritance. I often find myself using it to “extend” third party interfaces with useful additional behaviour.
Let’s say we wanted to add a map method to List that allows us to convert from a list of one type to a list of another. There is already such a method on the Stream interface, but it serves as an example
We used to have to either
a) Subclass a concrete List implementation and add our method (which makes re-use hard), or
b) Re-implement the considerably large List interface, delegating to a wrapped List.
You can ask your IDE to generate these delegate methods for you, but with a large interface like List the boilerplate tends to obscure the added behaviour.
class MappingList<T> implements List<T> { private List<T> impl; public int size() { return impl.size(); } public boolean isEmpty() { return impl.isEmpty(); } // Many more boilerplate methods omitted for brevity // The method we actually wanted to add. public <R> List<R> map(Function<T,R> f) { return list.stream().map(f).collect(Collectors.toList()); } } |
Guava gave us a third option
c) Extend the the Guava ForwardingList class. Unfortunately that meant you couldn’t extend any other class.
Java 8 gives us a fourth option
d) We can implement the forwarding behaviour in an interface, and then add our behaviour on top.
The disadvantage is you need a public method which exposes the underlying implementation. The advantages are you can keep the added behaviour separate, and it’s easier to compose them.
Our decorator can now be really short – something like
class MappableList<T> implements List<T>, ForwardingList<T>, Mappable<T> { private List<T> impl; public MappableList(List<T> impl) { this.impl = impl; } @Override public List<T> impl() { return impl; } } |
We can use it like this
// prints 3, twice. new MappableList<String>(asList("foo", "bar")) .map(s -> s.length()) .forEach(System.out::println); |
The new method we added is declared in its own Mappable<T> interface which is uncluttered.
interface Mappable<T> extends ForwardingList<T> { default <R> List<R> map(Function<T,R> f) { return impl().stream().map(f).collect(Collectors.toList()); } } |
The delegation boilerplate we can keep in its own interface, out of the way. Since it’s an interface we are free to extend other classes/interfaces in our decorator
interface ForwardingList<T> extends List<T> { List<T> impl(); default int size() { return impl().size(); } default boolean isEmpty() { return impl().isEmpty(); } // Other methods omitted for brevity } |
If we wanted to mix in some more functionality to our MappableList decorator class we could just implement another interface. In the above example we added a new method, so this time let’s modify one of the existing methods on List. Let’s make a List that always thinks it’s empty.
interface AlwaysEmpty<T> extends ForwardingList<T> { default boolean isEmpty() { return true; } } class MappableList<T> implements List<T>, ForwardingList<T>, Mappable<T>, AlwaysEmpty<T> { // Mix in the new interface // ... } |
Now our list always claims it’s empty
// prints true System.out.println(new MappableList<String>(asList("foo", "bar")).isEmpty()); |
Sebastian Millies
Very interesting!
Here’s a talk by Heinz Kabutz in which he outlines a related idea: http://javaspecialists.eu/talks/pdfs/2014%20JavaDay%20in%20Athens,%20Greece%20-%20%22Using%20Lambdas%20to%20Write%20Mixins%20in%20Java%208%22%20by%20Heinz%20Kabutz.pdf