kotlin-delegation-pattern
Delegation Pattern in Kotlin
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
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
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.