article banner

Interface Delegation

This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.

Kotlin has a feature called interface delegation. This is special support for the delegation pattern, so let’s discuss this first.

The delegation pattern

Imagine you have a class that implements an interface. In the example below, this is the GenericCreature class, which implements the Creature interface. If you want another class, Goblin, to behave just like GenericCreature, you can achieve this with composition by creating an instance of GenericCreature, keeping it as a property, and using its methods. This new class can also implement the Creature interface.

interface Creature { val attackPower: Int val defensePower: Int fun attack() } class GenericCreature( override val attackPower: Int, override val defensePower: Int, ) : Creature { override fun attack() { println("Attacking with $attackPower") } } class Goblin : Creature { private val delegate = GenericCreature(2, 1) override val attackPower: Int = delegate.attackPower override val defensePower: Int = delegate.defensePower override fun attack() { delegate.attack() } // ... } fun main() { val goblin = Goblin() println(goblin.defensePower) // 1 goblin.attack() // Attacking with 2 }

This is an example of a delegation pattern. The Goblin class delegates methods defined by the Creature interface to an object of type GenericCreature. The delegate property in the example above is a delegate, and the attack method and attackPower and defensePower properties are delegated.

To reuse the same method implementation, it is enough to use composition, which means keeping a property with an object of another type and using it on its own methods. The delegation pattern also involves implementing the same interface as the class we delegate to, thus introducing polymorphic behavior. The GenericCreature and Goblin classes both implement the Creature interface, therefore they can be used interchangeably in some cases.

Delegation and inheritance

Delegation is an old pattern and has always been presented as an alternative to inheritance. Truth be told, similar behavior can be achieved if we make GenericCreature open and we make Goblin extend it.

interface Creature { val attackPower: Int val defensePower: Int fun attack() } open class GenericCreature( override val attackPower: Int, override val defensePower: Int, ) : Creature { override fun attack() { println("Attacking with $attackPower") } } class Goblin : GenericCreature(2, 1) { // ... } fun main() { val goblin = Goblin() println(goblin.defensePower) // 1 goblin.attack() // Attacking with 2 }

Using inheritance seems easier, but it has 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 very strong relationship, but this is often not what we want. Maybe we don't want Goblin to be a GenericCreature because we are not able to guarantee that the former will always behave exactly like the latter.
  • Most classes are not designed for inheritance as they are either closed or we just should not inherit from them.
  • Inheritance breaks encapsulation, thus causing safety threats (see Item 36: Prefer composition over inheritance from Effective Kotlin).

All in all, this is why we often prefer to use delegation instead of inheritance. Fortunately, the creators of Kotlin introduced special support to help us with this.

Kotlin interface delegation support

Kotlin introduced special support for the interface delegation pattern, which makes it as easy to use as 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:

interface Creature { val attackPower: Int val defensePower: Int fun attack() } class GenericCreature( override val attackPower: Int, override val defensePower: Int, ) : Creature { override fun attack() { println("Attacking with $attackPower") } } class Goblin : Creature by GenericCreature(2, 1) { // ... } fun main() { val goblin = Goblin() println(goblin.defensePower) // 1 goblin.attack() // Attacking with 2 }

On the right side of the by keyword, there is a constructor call that creates an instance of GenericCreature that is used as a delegate. Under the hood, this delegate will be stored in a property, and all methods from the Creature interface will be implemented such 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.

class Goblin : Creature by GenericCreature(2, 1) // or class Goblin( att: Int, def: Int ) : Creature by GenericCreature(att, def) // or class Goblin( creature: Creature ) : Creature by creature // or class Goblin( val creature: Creature = GenericCreature(2, 1) ) : Creature by creature // or val creature = GenericCreature(2, 1) class Goblin : Creature by creature

We can use interface delegation multiple times in the same class.

class Goblin : Attack by Dagger(), Defense by LeatherArmour() class Amphibious : Car by SimpleCar(), Boat by MotorBoat()

When we use interface delegation, we can still override some methods from the interface ourselves. In such cases, these methods will not be generated automatically and will not call delegates by themselves.

interface Creature { val attackPower: Int val defensePower: Int fun attack() } class GenericCreature( override val attackPower: Int, override val defensePower: Int, ) : Creature { override fun attack() { println("Attacking with $attackPower") } } class Goblin : Creature by GenericCreature(2, 1) { override fun attack() { println("Special Goblin attack $attackPower") } } fun main() { val goblin = Goblin() println(goblin.defensePower) // 1 goblin.attack() // Special Goblin attack 2 }

The problem is that there is currently no way to reference the delegate's implicit property. So, if we need to do this, we typically make a primary constructor property that we delegate to and that we use when we need to reference the delegate.

interface Creature { val attackPower: Int val defensePower: Int fun attack() } class GenericCreature( override val attackPower: Int, override val defensePower: Int, ) : Creature { override fun attack() { println("Attacking with $attackPower") } } class Goblin( private val creature: Creature = GenericCreature(2, 1) ) : Creature by creature { override fun attack() { println("It will be special Goblin attack!") creature.attack() } } fun main() { val goblin = Goblin() goblin.attack() // It will be a special Goblin attack! // Attacking with 2 }

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 don’t mean a method because this can be added using an extension function. I mean an annotation that might be needed by a library. Consider the following example: for Jetpack Compose, you need to use an object that has the Immutable and Keep annotations, but you want to use the List interface, which is read-only but does not have these annotations. The simplest solution is to make a simple wrapper over List and use interface delegation to easily make our wrapper class also implement the List interface. In this way, all the methods that we can invoke on List can also be invoked on the wrapper.

@Immutable data class ComposeImmutableList<T>( val innerList: List<T> ) : List<T> by innerList

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 observed easily 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 that specifies the collect method, which can be used easily from Swift. More about this case in the Using Kotlin Multiplatform section.

class StateFlow<T>( source: StateFlow<T>, private val scope: CoroutineScope ) : StateFlow<T> by source { fun collect(onEach: (T) -> Unit) { scope.launch { collect { onEach(it) } } } }

The 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, we decorate it with BufferedInputStream to add buffering, then we decorate it with ZipInputStream to add unzipping capabilities, and finally we decorate it with ObjectInputStream to read an object.

var fis: InputStream = FileInputStream("/someFile.gz") var bis: InputStream = BufferedInputStream(fis) var gis: InputStream = ZipInputStream(bis) var ois = ObjectInputStream(gis) var someObject = ois.readObject() as SomeObject

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.

fun <T> Sequence<T>.filter( predicate: (T) -> Boolean ): Sequence<T> { return FilteringSequence(this, true, predicate) }

The decorator pattern uses delegation. Each decorator class needs to access the decorated object, use it as a delegate, and add behavior to some of its methods.

interface AdFilter { fun showToPerson(user: User): Boolean fun showOnPage(page: Page): Boolean fun showOnArticle(article: Article): Boolean } class ShowOnPerson( val authorKey: String, val prevFilter: AdFilter = ShowAds ) : AdFilter { override fun showToPerson(user: User): Boolean = prevFilter.showToPerson(user) override fun showOnPage(page: Page) = page is ProfilePage && page.userKey == authorKey && prevFilter.showOnPage(page) override fun showOnArticle(article: Article) = article.authorKey == authorKey && prevFilter.showOnArticle(article) } class ShowToLoggedIn( val prevFilter: AdFilter = ShowAds ) : AdFilter { override fun showToPerson(user: User): Boolean = user.isLoggedIn override fun showOnPage(page: Page) = prevFilter.showOnPage(page) override fun showOnArticle(article: Article) = prevFilter.showOnArticle(article) } object ShowAds : AdFilter { override fun showToPerson(user: User): Boolean = true override fun showOnPage(page: Page): Boolean = true override fun showOnArticle(article: Article): Boolean = true } fun createWorkshopAdFilter(workshop: Workshop) = ShowOnPerson(workshop.trainerKey) .let(::ShowToLoggedIn)

Interface delegation can help us avoid implementing methods that only call similar decorated object methods. This makes our implementation clearer and lets a reader focus on what is essential.

class Page class Article(val authorKey: String) class User(val isLoggedIn: Boolean) interface AdFilter { fun showToPerson(user: User): Boolean fun showOnPage(page: Page): Boolean fun showOnArticle(article: Article): Boolean } class ShowOnPerson( val authorKey: String, val prevFilter: AdFilter = ShowAds ) : AdFilter by prevFilter { override fun showOnPage(page: Page) = page is ProfilePage && page.userKey == authorKey && prevFilter.showOnPage(page) override fun showOnArticle(article: Article) = article.authorKey == authorKey && prevFilter.showOnArticle(article) } class ShowToLoggedIn( val prevFilter: AdFilter = ShowAds ) : AdFilter by prevFilter { override fun showToPerson(user: User): Boolean = user.isLoggedIn } object ShowAds : AdFilter { override fun showToPerson(user: User): Boolean = true override fun showOnPage(page: Page): Boolean = true override fun showOnArticle(article: Article): Boolean = true } fun createWorkshopAdFilter(workshop: Workshop) = ShowOnPerson(workshop.trainerKey) .let(::ShowToLoggedIn)

Intersection types

It is important to know that interface delegation can be used multiple times in the same class. Thanks to this, you can have a class that implements two interfaces, each of which is delegated to a different delegate. Such a class can aggregate the types and behavior of multiple other classes, so we call such a class represents an "intersection type". For example, in the Arrow library there is a ScopedRaise class that is a decorator for both EffectScope and CoroutineScope.

public class ScopedRaise<E>( raise: EffectScope<E>, scope: CoroutineScope ) : CoroutineScope by scope, EffectScope<E> by raise

The ScopedRaise class can represent both interfaces it implements at the same time; so, when we use it as a receiver, methods from both EffectScope and CoroutineScope can be used implicitly.

public suspend fun <E, A, B> Iterable<A>.parMapOrAccumulate( context: CoroutineContext = EmptyCoroutineContext, transform: suspend ScopedRaise<E>.(A) -> B ): Either<NonEmptyList<E>, List<B>> = coroutineScope { map { async(context) { either { val scope = this@coroutineScope transform(ScopedRaise(this, scope), it) } } }.awaitAll().flattenOrAccumulate() } suspend fun test() { listOf("A", "B", "C") .parMapOrAccumulate { v -> this.launch { } // We can do that, // because receiver is CoroutineContext this.ensure(v in 'A'..'Z') { error("Not letter") } // We can do that, because receiver is EffectScope } }

Another example comes from the Ktor framework’s integration tests. In this framework, you can use the testApplication function, which helps start a server for testing. It provides the ApplicationTestBuilder receiver, which extends TestApplicationBuilder and implements ClientProvider. For my integration tests, I define my own function because I want to have a similar receiver, but I often lack a couple of other properties. I would prefer to have a receiver that aggregates methods from multiple classes or interfaces, as well as useful properties like a background scope or a created test application reference. I often define such a receiver class using interface delegation.

class IntegrationTestScope( applicationTestBuilder: ApplicationTestBuilder, val application: Application, val backgroundScope: CoroutineScope, ) : TestApplicationBuilder(), ClientProvider by applicationTestBuilder

Limitations

The biggest limitation of the interface delegation pattern is that objects we delegate to must have an interface, and only methods from this interface will be delegated. You should also note that - unlike when we use inheritance - the meaning of the this reference does not change.

interface Creature { fun attack() } class GenericCreature : Creature { override fun attack() { // this.javaClass.name is always GenericCreature println("${this::class.simpleName} attacks") } fun walk() {} } class Goblin : Creature by GenericCreature() open class WildAnimal : Creature { override fun attack() { // this.javaClass.name depends on actual class println("${this::class.simpleName} attacks") } } class Wolf : WildAnimal() fun main() { GenericCreature().attack() // GenericCreature attacks Goblin().attack() // GenericCreature attacks WildAnimal().attack() // WildAnimal attacks Wolf().attack() // Wolf attacks // Goblin().walk() COMPILATION ERROR, no such method }

Conflicting elements from parents

There might be a situation in which two interfaces that our class uses for interface delegation define the same method or property, so we must resolve this conflict by overriding this element in the class. In the example below, both the Attack and Defense interfaces define the defense property, so we must override it in the Goblin class and specify how it should behave.

interface Attack { val attack: Int val defense: Int } interface Defense { val defense: Int } class Dagger : Attack { override val attack: Int = 1 override val defense: Int = -1 } class LeatherArmour : Defense { override val defense: Int = 2 } class Goblin( private val attackDelegate: Attack = Dagger(), private val defenseDelegate: Defense = LeatherArmour(), ) : Attack by attackDelegate, Defense by defenseDelegate { // We must override this property override val defense: Int = defenseDelegate.defense + attackDelegate.defense }

Summary

Interface delegation is not a very popular Kotlin feature, but it has use cases where it helps us avoid repetition of 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.