Effective Class Delegation
2020-09-29 • Márton Braun
One of the most significant items of the Effective Java book is Item 18: Favor composition over inheritance. To oversimplify its contents:
Inheritance is a popular way to reuse code, by extending a class that has the functionality you need. However, it’s also very error prone. It violates encapsulation, because the subclass depends on the internal implementation details of the superclass.
Problem statement
Here’s the original example used in the book:
// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
int addCount = 0;
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
}
This is a HashSet
subclass that’s supposed to count the number of elements (attempted to be) inserted into it, however, it’s broken. It turns out that the superclass uses the add
method in its implementation of addAll
, causing this class to count every element added to it using addAll
twice:
val set = InstrumentedHashSet<Int>()
set.addAll(listOf(1, 2, 3, 4, 5))
println(set.addCount) // 10
We could fix this by assuming that this will always be the case, and simply remove the override of addAll
. However, this would break if the implementation of the superclass changed in a newer version. We could try detecting whether this happens using some kind of a flag… But it would get quite complex.
The Java solution
So what can we do instead? As the item suggests, we can use composition over inheritance. Contain an instance of HashSet
in our own custom implementation, instead of extending it:
public class InstrumentedSet<E> implements Set<E> {
int addCount = 0;
private final Set<E> set;
public InstrumentedSet(Set<E> set) { this.set = set; }
public boolean add(E e) {
addCount++;
return set.add(e);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}
// ...
}
With this change, we’d have to provide the Set
to wrap as a parameter at the use site:
val set = InstrumentedSet<Int>(HashSet())
set.addAll(listOf(1, 2, 3, 4, 5))
The problem with this solution, then, is that we’re now implementing the entirety of the Set
interface ourselves. We have add
and addAll
covered, but this interface requires twelve more methods! This would all be boilerplate, where each method would just forward calls to the contained set
instance.
Effective Java proposes the introduction of an intermediate ForwardingSet
class, which InstrumentedSet
can then inherit from, and override just the two methods that it needs to intercept.
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
/* Lots of more methods... */
}
public class InstrumentedSet<E> extends ForwardingSet<E> {
int addCount = 0;
public InstrumentedSet(Set<E> s) { super(s); }
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
}
This is probably the best that Java can do here.
Going Kotlin
Now, let’s implement InstrumentedSet
in Kotlin instead. We’ll do this using class delegation. This allows us to implement an interface by delegating it to another object, which is exactly what we’re doing manually in the Java example above.
class InstrumentedSet<E>(private val set: MutableSet<E>) : MutableSet<E> by set
We’re using
MutableSet
as Kotlin’s equivalent interface tojava.util.Set
here.
Now our InstrumentedSet
implements the MutableSet
interface via the set
property. Whenever a method is invoked on it, it will simply invoke the same method on the contained set
. This is a one-liner implementation of FowardingSet
! This can be easily confirmed by taking a look at the generated bytecode, which looks something like this when decompiled to Java:
public final class InstrumentedSet implements Set {
private final Set set;
public InstrumentedSet(@NotNull Set set) {
this.set = set;
}
public int getSize() {
return this.set.size();
}
public boolean add(Object element) {
return this.set.add(element);
}
public void clear() {
this.set.clear();
}
// ...
}
All that’s left to do then is to modify the add
and addAll
methods, to count the attempted insertions:
class InstrumentedSet<E>(
private val set: MutableSet<E> = HashSet()
) : MutableSet<E> by set {
var addCount = 0
override fun add(element: E): Boolean {
addCount++
return set.add(element)
}
override fun addAll(elements: Collection<E>): Boolean {
addCount += elements.size
return set.addAll(elements)
}
}
We’ve also added a default value for the set
parameter here, a simple HashSet
. Clients can still pass in other MutableSet
implementations, but they are no longer required to do so, making the class more convenient to use.
And that’s it, we have a working InstrumentedSet
implementation. All the other MutableSet
methods that we haven’t implemented will continue to forward to set
. Everything works as expected now:
val set = InstrumentedSet<Int>()
set.addAll(listOf(1, 2, 3, 4, 5))
println(set.addCount) // 5
Wrap-up
Interestingly, this feature - also referred to as implementation by delegation - has been named as the “worst” feature in Kotlin by the lead language designer, Andrey Breslav on several occasions (e.g. during the KotlinConf 2018 closing panel discussion). There are some cases where this kind of delegation can get complicated and produce some… Interesting behaviour. However, in simple cases, it can rid you of a lot of tedious code.
To learn about a different kind of Kotlin delegate, have a look at Delightful Delegate Design and Krate, a better SharedPreferences experience.
You might also like...
Data classes aren't (that) magical
Data classes are great, but don't underestimate what a regular Kotlin class can do on its own.
Krate, a better SharedPreferences experience
Accessing SharedPreferences using its API directly can be somewhat inconvenient. Krate is a library built on Kotlin delegates to simplify the use of SharedPreferences.
Mastering API Visibility in Kotlin
When designing a library, minimizing your API surface - the types, methods, properties, and functions you expose to the outside world - is a great idea. This doesn't apply to just libraries: it's a consideration you should make for every module in a multi-module project.
Coroutine Cancellation 101
A brief introduction to the basics of coroutine cancellation.