1. Overview
There are many use cases in which delegation is preferred to inheritance. Kotlin has great language-level support for this.
In this tutorial, we’ll talk about Kotlin’s native support for the delegation pattern and see it in action.
2. Implementation
First, let’s assume we have a code example with the structure below in a third-party library:
interface Producer {
fun produce(): String
}
class ProducerImpl : Producer {
override fun produce() = "ProducerImpl"
}
Next, let’s decorate the existing implementation using the “by” keyword and add the additional necessary processing:
class EnhancedProducer(private val delegate: Producer) : Producer by delegate {
override fun produce() = "${delegate.produce()} and EnhancedProducer"
}
So, in this example, we’ve indicated that the EnhancedProducer class will encapsulate a delegate object of type Producer. And, it can also use functionality from the Producer implementation.
Finally, let’s verify that it works as expected:
val producer = EnhancedProducer(ProducerImpl())
assertThat(producer.produce()).isEqualTo("ProducerImpl and EnhancedProducer")
3. Use Cases
Now, let’s look at two common use cases for the delegation pattern.
First, we can use the delegation pattern to implement multiple interfaces using existing implementations:
class CompositeService : UserService by UserServiceImpl(), MessageService by MessageServiceImpl()
Second, we can use delegation to enhance an existing implementation.
The latter is what we did in the previous section. But, a more real-world example like the one below is especially useful when we can’t modify an existing implementation – for example, third-party library code:
class SynchronizedProducer(private val delegate: Producer) : Producer by delegate {
private val lock = ReentrantLock()
override fun produce(): String {
lock.withLock {
return delegate.produce()
}
}
}
4. Delegation Is Not Inheritance
Now, we need to always remember that the delegate knows nothing about the decorator. So, we shouldn’t try the GoF Template Method-like approach with them.
Let’s consider an example:
interface Service {
val seed: Int
fun serve(action: (Int) -> Unit)
}
class ServiceImpl : Service {
override val seed = 1
override fun serve(action: (Int) -> Unit) {
action(seed)
}
}
class ServiceDecorator : Service by ServiceImpl() {
override val seed = 2
}
Here, the delegate (ServiceImpl) uses a property defined in the common interface, and we override it in the decorator (ServiceDecorator). However, it doesn’t affect the delegate’s processing:
val service = ServiceDecorator()
service.serve {
assertThat(it).isEqualTo(1)
}
Finally, it’s important to note that, in Kotlin, we can delegate not only to interfaces but to separate properties as well.
5. Conclusion
In this tutorial, we talked about Kotlin interface delegation – when it should be used, how to configure it, and its caveats.
As usual, the complete source code for this article is available over on GitHub.