Kotlin delegation is a design pattern where one object delegates certain responsibilities or behaviors to another object. This pattern is supported directly by the Kotlin language through the use of delegation by by
keyword.
In simple terms, Kotlin delegation is a way for one object to hand off certain tasks or actions to another object. This is directly supported by Kotlin using the by
keyword. It’s like asking someone else to do part of your job for you.
It allows the derived class to access all the implemented public methods of an interface through a specific object.
Before delegation, we often faced code duplication and tight coupling issues when trying to reuse code across different classes or modules. For example, if we had multiple classes needing similar functionality, we’d end up duplicating code, leading to maintenance headaches.
Consider the scenario of logging in different parts of an application. We might have classes for processing orders, managing users, etc., all of which need logging. Previously, we might have subclassed each of these from a Logger
class or implemented logging directly within each class. This approach led to tightly coupled code and made it difficult to change or extend logging behavior without modifying each class.
Delegation solves these problems by allowing us to compose objects together instead of subclassing. With delegation, each class can focus on its primary responsibility, while delegating specific tasks to other objects. This promotes code reuse, separation of concerns, and flexibility.
Example of Delegation:
Without Using Delegation
If you don’t use delegation, you would need to manually implement the Logger
interface methods in the Service
class. Here’s how you would do it without delegation:
interface Logger {
fun log(message: String)
}
class FileLogger : Logger {
override fun log(message: String) {
println("Logging to file: $message")
}
}
class DatabaseLogger : Logger {
override fun log(message: String) {
println("Logging to database: $message")
}
}
class Service(private val logger: Logger) : Logger {
override fun log(message: String) {
logger.log(message) // Forwarding the log message to the logger instance
}
fun doSomething() {
log("Doing something") // Using the log() method of the logger instance
}
}
fun main() {
val fileLogger = FileLogger()
val serviceWithFileLogger = Service(fileLogger)
serviceWithFileLogger.doSomething() // Logs to file
val databaseLogger = DatabaseLogger()
val serviceWithDatabaseLogger = Service(databaseLogger)
serviceWithDatabaseLogger.doSomething() // Logs to database
}
- Logger Interface: Defines a contract with a method
log(message: String)
for logging. - FileLogger and DatabaseLogger Classes: Implement the
Logger
interface with specific implementations for logging to a file and logging to a database, respectively. - Service Class: Takes an instance of
Logger
in its constructor and implements theLogger
interface itself. It delegates thelog()
method to the provided logger instance and provides a methoddoSomething()
that uses the logging functionality. - Main Function: Creates instances of
FileLogger
andDatabaseLogger
and passes them to instances ofService
. This showcases howService
can use different logging mechanisms based on the logger instance provided
Using Delegation:
Sure! Here’s the example using delegation:
class Service(logger: Logger) : Logger by logger { // Delegation
// No need to explicitly implement log() method
fun doSomething() {
log("Doing something") // Delegating log() to the logger instance
}
}
fun main() {
val fileLogger = FileLogger()
val serviceWithFileLogger = Service(fileLogger)
serviceWithFileLogger.doSomething() // Logs to file
val databaseLogger = DatabaseLogger()
val serviceWithDatabaseLogger = Service(databaseLogger)
serviceWithDatabaseLogger.doSomething() // Logs to database
}
In this version, the Service
class implements the Logger
interface by delegation using the by
keyword followed by the instance of the Logger
interface (logger
) passed to its constructor. This means that Service
will delegate all calls to the log()
method to the provided logger
instance. Thus, there’s no need to explicitly implement the log()
method in the Service
class. This simplifies the code and promotes code reuse and separation of concerns.
Difference:
- Without delegation,
had to implement all the functions of theService
Logger
interface and delegate each of them to the providedLogger
instance. - With delegation, Kotlin automatically forwards all calls to the methods of
Logger
to the provided instance, saving us from writing boilerplate delegation code.
Benefits of using delegation:
In First example, the Service
class delegates logging responsibilities to an external logger instance using Kotlin’s delegation feature. Let’s break down the benefits of this approach:
- Code Reuse:By using delegation, the
Service
class can reuse existing logging implementations (FileLogger
andDatabaseLogger
) without needing to duplicate code. This promotes cleaner, more maintainable codebases, as we avoid rewriting the logging logic in multiple places. - Separation of Concerns:The
Service
class focuses solely on its primary responsibility, which is to perform some operation (doSomething()
). By delegating logging to an external logger instance, we separate the concerns of performing the operation and logging. This separation makes the code easier to understand and maintain, as each class has a clear and distinct responsibility. - Flexibility:Since the
Service
class accepts any object that implements theLogger
interface, it remains flexible in terms of logging implementations. We can easily switch between different logging mechanisms (e.g., logging to a file or logging to a database) by passing different logger instances toService
, without needing to modify its code. This promotes the open-closed principle, where classes are open for extension but closed for modification. - Reduced Coupling:The
Service
class is decoupled from specific logging implementations (FileLogger
andDatabaseLogger
). It interacts with logger instances through theLogger
interface, rather than concrete implementations. This reduces coupling between classes and makes the code more flexible and easier to test, as we can easily swap different logger implementations without affecting theService
class.
Use case:
1. Separate the logic of getters and setters :
Let’s create a simple example to illustrate how delegation can help separate the logic of getters and setters so that it can be reused.
Suppose we have a User
class with a property age
, and we want to ensure that the age is always within a certain range (e.g., between 0 and 100). We’ll create a separate class called AgeValidator
to handle the validation logic, and we’ll delegate the implementation of the getters and setters for the age
property to this validator class.
class AgeValidator {
private var fieldValue: Int = 0
operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return fieldValue
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
fieldValue = when {
value < 0 -> 0
value > 100 -> 100
else -> value
}
}
}
class User {
var age: Int by AgeValidator()
}
fun main() {
val user = User()
// Setting age to a valid value
user.age = 25
println("Age: ${user.age}") // Output: Age: 25
// Trying to set age to an invalid value
user.age = -10
println("Age: ${user.age}") // Output: Age: 0 (Age is automatically corrected to the minimum allowed value)
// Trying to set age to another invalid value
user.age = 150
println("Age: ${user.age}") // Output: Age: 100 (Age is automatically corrected to the maximum allowed value)
}
In this example:
- We define a separate
AgeValidator
class withgetValue
andsetValue
functions. These functions will be invoked when theage
property of theUser
class is accessed or modified. - The
getValue
function retrieves the value ofage
. - The
setValue
function validates the new value ofage
to ensure it falls within the desired range (0 to 100). If the value is outside this range, it is automatically corrected to the nearest valid value. - In the
User
class, we delegate the implementation of theage
property to an instance ofAgeValidator
. - In the
main
function, we demonstrate setting theage
property to both valid and invalid values, verifying that the validation logic works as expected.
2. Multiple Delegation
Kotlin supports both single and multiple delegation, enabling classes to delegate method calls to multiple objects. Let’s demonstrate multiple delegation with an example:
Suppose we have two interfaces representing different functionalities:
interface Swim {
fun swim()
}
interface Fly {
fun fly()
}
And we have two classes that implement these interfaces:
class Dolphin : Swim {
override fun swim() {
println("Dolphin is swimming")
}
}
class Bird : Fly {
override fun fly() {
println("Bird is flying")
}
}
Now, let’s create a class SuperCreature
that wants to exhibit both swimming and flying behaviors by delegating to instances of Dolphin
and Bird
:
class SuperCreature(swimmer: Swim, flyer: Fly) : Swim by swimmer, Fly by flyer {
fun performActions() {
swim()
fly()
}
}
In this example:
- The
SuperCreature
class delegates theswim()
method to theswimmer
instance and thefly()
method to theflyer
instance. - We use the
by
keyword followed by the instances ofSwim
andFly
interfaces to delegate the respective methods. - The
performActions()
method demonstrates how theSuperCreature
can perform both swimming and flying actions without directly implementing those functionalities.
Now, let’s create instances of Dolphin
and Bird
and pass them to SuperCreature
:
fun main() {
val dolphin = Dolphin()
val bird = Bird()
val superCreature = SuperCreature(dolphin, bird)
superCreature.performActions()
}
When we run the main()
function, it will output:
Dolphin is swimming
Bird is flying
This demonstrates how the SuperCreature
class delegates method calls to both Dolphin
and Bird
instances, allowing it to exhibit multiple behaviors through delegation.
In summary, by using delegation, we simplify code, promote code reuse, separation of concerns, flexibility, and reduced coupling, leading to cleaner, more maintainable, and testable codebases.
Also Read :