Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a pull request from a developer who was trying to implement a "Decorator" pattern to add logging to a complex Data Repository. He had created a wrapper class that implemented the same interface as the repository, but because the interface had twenty different methods, he had to manually override every single one of them just to call the underlying implementation. It was a sea of boilerplate code—hundreds of lines of repository.doSomething() inside this.doSomething(). It was tedious to read and a nightmare to maintain. That's when I pointed him toward the by keyword.

Cutting out the boilerplate with Class Delegation

In most languages, if you want to wrap a class to add some extra behavior without inheriting from it (composition), you end up writing those annoying forwarding methods. Kotlin solves this with class delegation. By using the by keyword in the class header, you tell the compiler: "Implement this interface, but instead of making me write the logic, just forward all the calls to this other object."

interface Printer {
    fun printMessage(msg: String)
    fun printError(err: String)
}

class RealPrinter : Printer {
    override fun printMessage(msg: String) = println("Printing: $msg")
    override fun printError(err: String) = println("Error: $err")
}

// This class implements Printer, but delegates everything to 'base'
class LoggingPrinter(base: Printer) : Printer by base {
    override fun printMessage(msg: String) {
        println("Log: About to print a message...")
        // We can still call the delegated implementation if we want, 
        // but usually, we just override the specific method we care about.
        base.printMessage(msg) 
    }
}

Notice that LoggingPrinter didn't have to override printError. Because of by base, the compiler automatically generated that forwarding method for us. I love this because it gives you the power of inheritance (the type is still a Printer) with the flexibility of composition. You aren't locked into a rigid class hierarchy; you're just wrapping behavior.

Handing off property logic to the 'by' keyword

Delegation isn't just for classes; it's incredibly powerful for properties. Property delegation means that the getter and setter of a variable are handled by a separate object. You've probably already used by lazy, which is the most common built-in delegate. It ensures a value is only computed the first time it's accessed.

But you can write your own. Imagine you want a property that logs every time it's changed, or one that validates the data before saving it. Instead of writing the same set(value) logic in ten different variables, you create a delegate class.

import kotlin.reflect.KProperty

class TrimDelegate {
    private var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        println("Trimming whitespace from ${property.name}")
        value = newValue.trim()
    }
}

class UserProfile {
    // Now, any string assigned to 'username' is automatically trimmed
    var username: String by TrimDelegate()
}

The magic here happens in the getValue and setValue operator functions. When you write user.username = " KotlinDev ", Kotlin doesn't actually assign that value to a field in UserProfile. Instead, it calls TrimDelegate.setValue(). It's a clean way to extract cross-cutting concerns—like validation, caching, or logging—out of your business logic and into reusable components.




📋 Practical Task

Exercise: Building a Cached Configuration Manager

You are building a system that loads configuration settings from a slow external database. To avoid hitting the database constantly, you need to implement a property delegate that caches the value after the first read.

Your Task:

  1. Create a class called CachedConfigDelegate.
  2. Implement the getValue operator function. Inside this function, it should check if a local cachedValue variable is null.
  3. If it is null, simulate a database call by printing "Fetching from DB..." and assigning a dummy string (e.g., "DB_VALUE") to the cache.
  4. If it is not null, print "Returning cached value" and return the stored value.
  5. Create a ConfigManager class with a property apiToken that uses your CachedConfigDelegate via the by keyword.
  6. In your main function, access apiToken three times and verify that the "Fetching from DB..." message only appears once.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.