Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
210: Class Delegation with the by Keyword In Depth
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
UserRepositorywith methods:getUser(id: String): StringandupdateUser(id: String, name: String). - Create a
SqlUserRepositorythat implements this interface (just useprintlnto simulate DB hits). - Create a
CachedUserRepositorythat uses class delegation (thebykeyword) to delegate to aUserRepositoryinstance. - Inside
CachedUserRepository, implement aprivate 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.
There are no comments for now.