Kotlin Class Delegation
Introduction
Delegation is a design pattern that allows object composition as an alternative to inheritance. Rather than extending a class to reuse its functionality, you delegate the work to another object. Kotlin provides a language-level support for delegation using the keyword by.
Class delegation in Kotlin is a powerful feature that helps you implement the composition over inheritance principle, making your code more flexible and maintainable. It allows a class to implement an interface by delegating all of its members to a specified object.
Understanding the Delegation Pattern
Before we dive into Kotlin's implementation, let's understand what delegation means in programming:
- Inheritance: "I am a type of something"
- Delegation: "I use something to do my work"
Delegation promotes composition over inheritance, which can lead to more flexible and maintainable code.
Basic Syntax of Class Delegation
The basic syntax for class delegation in Kotlin is:
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() {
println(x)
}
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
val derived = Derived(b)
derived.print() // Prints: 10
}
In this example:
Baseis an interface with aprint()functionBaseImplis a concrete implementation ofBaseDerivedclass implementsBaseby delegating to the provided instanceb- When we call
derived.print(), the call is delegated to the implementation inBaseImpl
How Delegation Works
When you use the by keyword, Kotlin automatically generates all the methods that forward to the delegated object. Behind the scenes, Kotlin creates wrapper methods in the Derived class that call the corresponding methods on the delegated object.
It's as if Kotlin generates this code for you:
class Derived(private val b: Base) : Base {
override fun print() {
b.print()
}
}
This saves you from writing repetitive delegation code.
Overriding Delegated Methods
You can override methods from the delegated interface if you need to modify behavior:
interface Base {
fun printMessage()
fun printMessageLine()
}
class BaseImpl(val x: Int) : Base {
override fun printMessage() { print(x) }
override fun printMessageLine() { println(x) }
}
class Derived(b: Base) : Base by b {
// This overrides the method from the delegate
override fun printMessage() { print("abc") }
}
fun main() {
val b = BaseImpl(10)
val derived = Derived(b)
derived.printMessage() // Prints: abc
derived.printMessageLine() // Prints: 10
}
In this example:
printMessage()is overridden inDerived, so the delegated implementation is not usedprintMessageLine()is not overridden, so it uses the delegated implementation
Multiple Interface Delegation
Kotlin allows delegating multiple interfaces to different objects:
interface Engine {
fun start()
}
interface Radio {
fun turnOn()
}
class EngineImpl : Engine {
override fun start() {
println("Engine started")
}
}
class RadioImpl : Radio {
override fun turnOn() {
println("Radio turned on")
}
}
class Car(engine: Engine, radio: Radio) : Engine by engine, Radio by radio
fun main() {
val car = Car(EngineImpl(), RadioImpl())
car.start() // Prints: Engine started
car.turnOn() // Prints: Radio turned on
}
This is a powerful way to compose functionality from different sources without complex inheritance hierarchies.
Property Delegation
In addition to method delegation, Kotlin also supports property delegation:
interface User {
val name: String
val age: Int
}
class UserImpl(override val name: String, override val age: Int) : User
class UserInfo(user: User) : User by user {
override val age: Int
get() = super.age + 1 // Modified property
}
fun main() {
val user = UserImpl("John", 30)
val userInfo = UserInfo(user)
println(userInfo.name) // Prints: John (delegated)
println(userInfo.age) // Prints: 31 (modified)
}
In this example, the name property is delegated, while age is overridden with custom logic.
Real-World Example: Window System
Let's see a more practical example of class delegation with a simplified window system:
interface WindowFeature {
fun render()
fun handleInput()
}
class ScrollFeature : WindowFeature {
override fun render() {
println("Rendering scrollbars")
}
override fun handleInput() {
println("Handling scroll events")
}
}
class BorderFeature : WindowFeature {
override fun render() {
println("Rendering borders")
}
override fun handleInput() {
println("Handling border resize events")
}
}
class Window(
private val scrollFeature: WindowFeature = ScrollFeature(),
private val borderFeature: WindowFeature = BorderFeature()
) {
// Delegate rendering to specialized feature implementations
fun renderScrollbars() = scrollFeature.render()
fun renderBorders() = borderFeature.render()
// Delegate input handling to specialized feature implementations
fun handleScrollInput() = scrollFeature.handleInput()
fun handleBorderInput() = borderFeature.handleInput()
// Main window methods that use delegated functionality
fun renderAll() {
println("Rendering window background")
renderScrollbars()
renderBorders()
}
}
fun main() {
val window = Window()
window.renderAll()
// Output:
// Rendering window background
// Rendering scrollbars
// Rendering borders
window.handleScrollInput() // Prints: Handling scroll events
}
This approach lets us compose a window from reusable feature components without complex inheritance.
When to Use Class Delegation
Class delegation is particularly useful in the following scenarios:
- Implementing the Decorator Pattern: When you need to add behaviors to objects without modifying their code
- Composition over Inheritance: When you want to favor object composition over class inheritance
- API Adaptation: When adapting one interface to another
- Testing: When you need to mock behavior for testing purposes
- Feature Composition: When building objects from multiple feature components
Delegation vs. Inheritance
| Delegation | Inheritance |
|---|---|
| Loose coupling | Tight coupling |
| Composition-based | Extension-based |
| More flexible at runtime | Fixed at compile time |
| Explicit relationship | Implicit relationship |
| Can delegate to multiple objects | Limited by single inheritance |
Summary
Kotlin class delegation provides a built-in solution for implementing the delegation pattern without boilerplate code. Using the by keyword, you can:
- Implement interfaces by delegating to other objects
- Override specific methods when needed
- Delegate to multiple interfaces simultaneously
- Create more flexible and maintainable code
Class delegation promotes composition over inheritance, leading to more modular and testable code structures. It's an essential tool in the Kotlin developer's toolbox for creating flexible and maintainable applications.
Additional Resources
Exercises
- Create an
AudioPlayerclass that delegates to bothMusicPlayerandPodcastPlayerinterfaces. - Implement a logging system that uses delegation to log different types of messages in different formats.
- Use class delegation to create a caching layer for a data repository.
- Implement a shape drawing system where a
CompositeShapedelegates to multiple other shapes. - Design a notification system that delegates to different notification channels (email, SMS, push).
If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)