You are currently viewing Kotlin Delegation

Kotlin Delegation

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
}
  1. Logger Interface: Defines a contract with a method log(message: String) for logging.
  2. FileLogger and DatabaseLogger Classes: Implement the Logger interface with specific implementations for logging to a file and logging to a database, respectively.
  3. Service Class: Takes an instance of Logger in its constructor and implements the Logger interface itself. It delegates the log() method to the provided logger instance and provides a method doSomething() that uses the logging functionality.
  4. Main Function: Creates instances of FileLogger and DatabaseLogger and passes them to instances of Service. This showcases how Service 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, Service had to implement all the functions of the Logger interface and delegate each of them to the provided Logger 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:

  1. Code Reuse:By using delegation, the Service class can reuse existing logging implementations (FileLogger and DatabaseLogger) without needing to duplicate code. This promotes cleaner, more maintainable codebases, as we avoid rewriting the logging logic in multiple places.
  2. 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.
  3. Flexibility:Since the Service class accepts any object that implements the Logger 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 to Service, without needing to modify its code. This promotes the open-closed principle, where classes are open for extension but closed for modification.
  4. Reduced Coupling:The Service class is decoupled from specific logging implementations (FileLogger and DatabaseLogger). It interacts with logger instances through the Logger 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 the Service 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 with getValue and setValue functions. These functions will be invoked when the age property of the User class is accessed or modified.
  • The getValue function retrieves the value of age.
  • The setValue function validates the new value of age 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 the age property to an instance of AgeValidator.
  • In the main function, we demonstrate setting the age 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 the swim() method to the swimmer instance and the fly() method to the flyer instance.
  • We use the by keyword followed by the instances of Swim and Fly interfaces to delegate the respective methods.
  • The performActions() method demonstrates how the SuperCreature 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 :