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 implements List {
private List 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 List map(Function 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 implements List, ForwardingList, Mappable {
private List impl;
public MappableList(List impl) {
this.impl = impl;
}
@Override
public List impl() {
return impl;
}
}
We can use it like this
// prints 3, twice.
new MappableList(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 extends ForwardingList {
default List map(Function 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 extends List {
List 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 extends ForwardingList {
default boolean isEmpty() {
return true;
}
}
class MappableList implements
List,
ForwardingList,
Mappable,
AlwaysEmpty { // Mix in the new interface
// ...
}
Now our list always claims it’s empty
// prints true
System.out.println(new MappableList(asList("foo", "bar")).isEmpty());