ylliX - Online Advertising Network

Effective Class Delegation


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 to java.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.

Data classes are great, but don’t underestimate what a regular Kotlin class can do on its own.

A brief introduction to the basics of coroutine cancellation.

Accessing SharedPreferences using its API directly can be somewhat inconvenient. Krate is a library built on Kotlin delegates to simplify the use of SharedPreferences.

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.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *