Skip to Content
Course content

210: Class Delegation with the by Keyword In Depth

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

I've been thinking about how to handle a common problem in the project I'm working on: adding functionality to a service without creating a massive, rigid inheritance hierarchy. I have a FileStorage interface, and a standard DiskStorage implementation. Now, I need a version that encrypts files before they hit the disk. My first instinct was to just extend DiskStorage, but that feels wrong. What if I later want to encrypt files being sent to CloudStorage? Inheritance would force me to duplicate the encryption logic for every storage type.

The Manual Delegation Headache

I started by trying a simple composition approach. I'll create an EncryptedStorage class that takes a FileStorage instance in its constructor and implements the interface manually.

interface FileStorage {
    fun save(fileName: String, data: String)
    fun load(fileName: String): String
    fun delete(fileName: String)
}

class DiskStorage : FileStorage {
    override fun save(fileName: String, data: String) = println("Saving $data to disk as $fileName")
    override fun load(fileName: String) = "Disk data"
    override fun delete(fileName: String) = println("Deleting $fileName from disk")
}

class EncryptedStorage(private val baseStorage: FileStorage) : FileStorage {
    override fun save(fileName: String, data: String) {
        val encrypted = "ENCRYPTED($data)"
        baseStorage.save(fileName, encrypted)
    }

    override fun load(fileName: String): String {
        val data = baseStorage.load(fileName)
        return data.removePrefix("ENCRYPTED(").removeSuffix(")")
    }

    override fun delete(fileName: String) {
        baseStorage.delete(fileName)
    }
}

This works, but look at the delete method. I'm just passing the call through to baseStorage. If FileStorage had twenty methods and I only wanted to change two, I'd be writing eighteen "pass-through" methods. That's pure boilerplate, and it's exactly the kind of noise I want to eliminate.

Letting the 'by' Keyword take over

This is where Kotlin's class delegation comes in. I can tell the compiler: "Implement this interface, and for every method I don't explicitly override, just use this other object." I'll rewrite EncryptedStorage using the by keyword.

class EncryptedStorage(private val baseStorage: FileStorage) : FileStorage by baseStorage {
    override fun save(fileName: String, data: String) {
        val encrypted = "ENCRYPTED($data)"
        baseStorage.save(fileName, encrypted)
    }

    override fun load(fileName: String): String {
        val data = baseStorage.load(fileName)
        return data.removePrefix("ENCRYPTED(").removeSuffix(")")
    }
    // Note: I've completely removed the delete() method!
}

Now, if I call encryptedStorage.delete("test.txt"), Kotlin automatically routes that call to baseStorage.delete(). It's as if the compiler wrote that boring pass-through method for me. I only have to write the logic I actually care about—the encryption and decryption.

Testing the Boundaries of the Delegate

One thing I wondered was: "If I delegate the interface, can I still access the original object?" In the example above, I kept baseStorage as a private val. But if I had just passed it as a constructor parameter without the val, I wouldn't have a reference to it inside my overrides.

Let's try a quick experiment. If I define it as class EncryptedStorage(base: FileStorage) : FileStorage by base, I can no longer call base.save() inside my save override because base is only available during initialization, not as a member of the class. To actually augment the behavior (like we did with encryption), you almost always want to declare that delegate as a property (val or var).

Another interesting quirk: delegation happens at the interface level. If DiskStorage had a public method called checkDiskSpace() that wasn't part of the FileStorage interface, EncryptedStorage would NOT have that method, even though it's delegating to a DiskStorage instance. It only delegates the members defined in the interface it's implementing. This is actually a good thing—it keeps our API surfaces clean and prevents leaking implementation details of the delegate.




📋 Practical Task

Implementing a Cached Data Repository using Class Delegation

You are building a data layer for an app. You have a UserRepository interface that handles fetching user profiles. You have a SqlUserRepository that hits a database (which is slow). You need to create a CachedUserRepository that delegates all work to the SQL implementation but intercepts the getUser call to check a local cache first.

Requirements:

  • Create an interface UserRepository with methods: getUser(id: String): String and updateUser(id: String, name: String).
  • Create a SqlUserRepository that implements this interface (just use println to simulate DB hits).
  • Create a CachedUserRepository that uses class delegation (the by keyword) to delegate to a UserRepository instance.
  • Inside CachedUserRepository, implement a private val cache = mutableMapOf<String, String>().
  • Override getUser: if the user is in the cache, return it; otherwise, fetch it from the delegate, save it to the cache, and then return it.
  • Override updateUser: call the delegate to update the DB, and then update (or clear) the cache for that user.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.