Interface Delegation
This is a chapter from the book Advanced Kotlin. You can find it on LeanPub.
Kotlin has a feature called interface delegation, which is a special support for the delegation pattern. Let's discuss the delegation pattern first.
Delegation pattern
Consider that you have a class that implements an interface. In the below example, it is the GenericCreature
class implementing the Creature
interface. If you want another class, Goblin
, to behave just like GenericCreature
, you can achieve that using composition by creating an instance of GenericCreature
, keeping it as a property, and using its methods. This new class can also implement the interface Creature
. Then we can talk about delegation.
This is an example of a delegation pattern. The class Goblin
delegates methods defined by the interface Creature
to an object of type GenericCreature
. The delegate
property in the above example is a delegate, and the method attack
and properties attackPower
and defensePower
are delegated.
To reuse the same method implementation, it is enough to use composition, so to keep a property with an object of another type and use it on its own methods. The delegation pattern also includes implementing the same interface, which introduces polymorphic behavior. Classes GenericCreature
and Goblin
both implement the Creature
interface, so in some cases, they can be used interchangeably.
Delegation and inheritance
From the beginning, the delegation pattern was presented as an alternative to inheritance. In the end, similar behavior can be achieved if we make GenericCreature
open and we make Goblin
extend it.
Using inheritance seems easier, but there are some consequences and limitations that make us choose delegation anyway:
- We can inherit from only one class, but we can delegate to many objects.
- Inheritance is a really strong relationship, and we often do not want it. Maybe we do not want
Goblin
to be aGenericCreature
because we are not able to guarantee that Goblin behaves as GenericCreature in all the ways. - Most classes are not designed for an inheritance, and they are either closed, or we just should not inherit from them.
- Inheritance breaks encapsulation, what generates safety threats (see Item 36: Prefer composition over inheritance from Effective Kotlin).
That is why we often prefer to use delegation instead of inheritance, and to support us, Kotlin creators introduced special support.
Kotlin interface delegation support
Kotlin introduced a special support for Interface Delegation that makes it as easy to use as an inheritance. After specifying an interface you want your class to implement, you can use the by
keyword and specify the object that should be used as a delegate. This removes the "writing additional code" overhead. This is how this could be used in Goblin
:
On the right side of the by
keyword, there is a constructor call that creates an instance of GenericCreature
. This instance is used as a delegate. Under the hood, this delegate will be stored in a property, and all methods from the interface Creature
will be implemented in a way so that they call appropriate methods from the delegate.
The object used as a delegate can be created using primary constructor parameters, or we can use a primary constructor parameter as a delegate. We can also use a variable from the outer scope as a delegate.
We can use interface delegation multiple times in the same class.
When we use interface delegation, we can still override some methods from the interface ourselves. In such cases, those methods will not be generated automatically, and they will not call delegates by themselves.
The problem is that there is currently no way to reference the delegate implicit property. That is why if we need to do that, we typically make a primary constructor property that we delegate to and that we use when we need to reference the delegate.
Wrapper classes
An interesting usage of interface delegation is to make a simple wrapper over an interface that adds something we could not add otherwise. I do not mean a method because it can be added using an extension function. I mean rather an annotation that some library might need. Consider the following example: For Jetpack Compose, you need to use an object that has Immutable
and Keep
annotations, but you want to use the List
interface, which is read-only but does not have those annotations. The simplest solution is to make a simple wrapper over List
and use interface delegation to easily make our wrapper class implement the List
interface as well. Thanks to that, all the methods that we can invoke on List
, we can also invoke on the wrapper.
Another example of a wrapper class is something that is used in some multiplatform mobile Kotlin projects. The problem is that in View Model classes, we like to expose observable properties of type StateFlow
, that can be easily observed in Android but not so easily in iOS. To make it easier to observe them, one solution is to define the following wrapper for them, which specifies the collect
method that can be easily used from Swift. More about this case in the Using Multiplatform Kotlin section.
Decorator pattern
Beyond a simple wrapper, there is also the Decorator pattern, which uses a class to decorate another class with new capabilities, but still implements the same interface (or extends the same class). So, for instance, when we make a FileInputStream
to read a file, then decorate it with BufferedInputStream
to add buffering, then we decorate it using ZipInputStream
to add unzipping capabilities, then we decorate it with ObjectInputStream
to read an object.
The decorator pattern is used by many libraries. Consider how sequence or flow processing works - each transformation decorates the previous sequence or flow with a new operation.
The decorator pattern uses delegation. Each decorator class needs to access the decorated object and use it as a delegate, plus add behavior to some of its methods.
Interface delegation can help us avoid implementing methods that only call similar decorated object methods. This makes our implementation more clear, as a reader can concentrate on what is essential.
Intersection types
It is important that interface delegation can be used multiple times in the same class. Thanks to that, this feature is sometimes used to implement a class that represents two interfaces. You can call it an intersection type. For example, in the Arrow library, there is a ScopedRaise
class that is a decorator for both EffectScope
and CoroutineScope
.
The ScopedRise
class can represent both interfaces it implements at the same time, and thanks to that, when we use it as a receiver, methods from both EffectScope
and CoroutineScope
can be used implicitly.
Limitations
The biggest limitation of Interface Delegation is that objects we delegate to must have an interface, and only methods from this interface will be delegated. You should also notice that this
reference does not change its meaning like when we use inheritance.
Conflicting elements from parents
There might be a situation where two interfaces our class uses for interface delegation define the same method or property. We must resolve this conflict by overriding this element in the class. In the example below, both Attack
and Defense
interfaces define the defense
property, so we must override it in the Goblin
class and specify how it should behave.
Summary
Interface delegation is not a very popular Kotlin feature, but it has its use cases where it repeats the need for boilerplate code. It is very simple - Kotlin implicitly generates methods and properties defined in an interface, and their implementations call similar methods from the delegate objects. Nevertheless, this feature can help us make our code more clear and concise. Interface delegation can be used when we need to make a simple wrapper over an interface, implement the decorator pattern, or make a class that collects methods from two interfaces. The biggest limitation of Interface Delegation is that objects we delegate to must have an interface, and only methods from this interface will be delegated.