Composing Your Way to Better Code: Why Kotlin's Delegation is a Game Changer
Stop writing boilerplate! See how the by keyword gives you the flexibility of composition with the conciseness of inheritance.
Have you ever found yourself in a coding dilemma, weighing the pros and cons of inheritance versus composition? It’s a classic software design debate, and for good reason. Each approach has its strengths and weaknesses, often leaving developers to choose between convenience and flexibility.
But what if you could have the best of both worlds? What if you could enjoy the conciseness of inheritance while retaining the incredible flexibility of composition? With Kotlin, you can, thanks to a powerful feature called Class Delegation.
Let’s dive into this fundamental concept and see how Kotlin makes our lives easier and our code stronger.
The Allure of Inheritance: Simple, But Sometimes Stifling
Inheritance is often one of the first object-oriented concepts we learn. It’s intuitive: “A SportsCar is a Car."
Consider a simple scenario for handling various digital files:
open class DigitalFile(val name: String, val sizeInBytes: Long) {
open fun displayInfo() {
//
println("File: $name, Size: $sizeInBytes bytes")
}
}
class ImageFile(name: String, sizeInBytes: Long, val resolution: String) : DigitalFile(name, sizeInBytes) {
override fun displayInfo() {
super.displayInfo() // Reuse parent's info display
println("Resolution: $resolution")
}
}
fun main() {
val myPhoto = ImageFile("vacation_pic.jpg", 2_000_000, "1920x1080")
myPhoto.displayInfo()
}What’s great about inheritance here?
- Subtyping: An
ImageFileis aDigitalFile, so we can treat it as one. This is great for polymorphism. - Code Reuse: Properties and methods are automatically available in
ImageFile.
But what are the downsides?
- Tight Coupling:
ImageFileis permanently tied to the implementation details ofDigitalFile. This creates a static dependency. IfDigitalFilechanges significantly,ImageFileis forced to change too, reducing maintainability. - Limited Flexibility: You are locked into a single inheritance chain. You can’t easily make an
ImageFilealso inherit the behavior of anEncryptedFileor aCompressedFile.
This tight coupling and limited flexibility are why seasoned developers often advise to “favor composition over inheritance.”
The Power of Composition: Flexibility at a Cost
Composition is like building with LEGO bricks. Instead of inheriting traits, an object contains other objects that provide specific behaviors. “A DocumentEditor has a SpellChecker."
Let’s refactor our file handling using composition. We define interfaces for our file behaviors:
interface FileDisplay {
fun display()
}
interface FileOperation {
fun performAction()
}
// A basic implementation for displaying
class SimpleFileDisplayer(val name: String, val size: Long) : FileDisplay {
override fun display() {
println("Basic File: $name, Size: $size bytes")
}
}
// A basic implementation for an operation (e.g., sharing)
class ShareableFile(val fileName: String) : FileOperation {
override fun performAction() {
println("Sharing file: $fileName via email...")
}
}
// Composing an AdvancedDocument
class AdvancedDocument(
private val displayComponent: FileDisplay,
private val operationComponent: FileOperation // Dependencies are injected via constructor
) {
fun showDocumentDetails() {
// Manual forwarding (boilerplate!)
displayComponent.display()
}
fun executeDocumentAction() {
// Manual forwarding (more boilerplate!)
operationComponent.performAction()
}
}
fun main() {
val basicDisplay = SimpleFileDisplayer("report.docx", 500_000)
val sharer = ShareableFile("report.docx")
// The AdvancedDocument is loosely coupled, depending only on the interfaces.
val salesReport = AdvancedDocument(basicDisplay, sharer)
salesReport.showDocumentDetails()
salesReport.executeDocumentAction()
}Why is this more flexible?
- Loose Coupling:
AdvancedDocumentdepends only on the interface contracts (FileDisplay,FileOperation), not on concrete classes likeSimpleFileDisplayer. You can easily swap implementations at any time (even runtime). - Mix-and-Match: You can combine any number of interfaces and behaviors without a restrictive hierarchy.
The Catch:
- Boilerplate: For every method you want the
AdvancedDocumentto expose, you must manually write a forwarding call (e.g.,displayComponent.display()). This boilerplate code quickly becomes tedious and noisy.
This manual forwarding is the pain point that prevents many developers from fully embracing the power of composition.
Enter Kotlin’s Class Delegation: The Best of Both Worlds
This is where Kotlin truly shines. It provides a linguistic construct that allows us to expose composed behavior as if it were inherited. This is Class Delegation, enabled by the by keyword.
Let’s re-imagine our AdvancedDocument using delegation:
interface FileDisplay {
fun display()
fun getFilename(): String
}
interface FileOperation {
fun performAction()
fun getStatus(): String
}
// Implementation for displaying
class DefaultFileDisplayer(val filename: String, val size: Long) : FileDisplay {
override fun display() {
println("Default Display - File: $filename, Size: $size bytes")
}
override fun getFilename(): String = filename
}
// Implementation for an operation
class CloudUploader(private val fileToUpload: String) : FileOperation {
override fun performAction() {
println("Uploading $fileToUpload to cloud...")
// NOTE: In production Kotlin code, use coroutine delay() instead of Thread.sleep()
// for non-blocking asynchronous operations.
Thread.sleep(500)
}
override fun getStatus(): String = "Completed"
}
// Our AdvancedDocument, now using delegation!
// It implements the interfaces *by* delegating to the provided implementations.
class AdvancedDocumentDelegated(
private val displayImpl: FileDisplay,
private val operationImpl: FileOperation
) : FileDisplay by displayImpl, FileOperation by operationImpl {
// 1. You can freely call delegated methods internally:
fun printDocumentSummary() {
println("Document Summary for ${getFilename()}:") // Calls delegated displayImpl.getFilename()
display() // Calls delegated displayImpl.display()
println("Operation: ${getStatus()}") // Calls delegated operationImpl.getStatus()
}
// 2. You can also override a delegated method to add custom logic:
override fun display() {
println("--- Starting Custom Display ---") // Custom behavior (e.g., logging)
displayImpl.display() // Call the original delegated implementation
println("--- End Custom Display ---")
}
}
fun main() {
val displayer = DefaultFileDisplayer("presentation.pptx", 10_000_000)
val uploader = CloudUploader("presentation.pptx")
val presentation = AdvancedDocumentDelegated(displayer, uploader)
presentation.display() // Calls our custom override, which then calls displayImpl.display()
presentation.performAction() // Automatically delegated to operationImpl.performAction()
}What just happened?
- Automatic Forwarding: Kotlin’s compiler automatically generates all the boilerplate forwarding methods for
FileDisplayandFileOperation. We got all the methods without writing a single manual call. - Ultimate Flexibility: We retained the loose coupling of composition. The
presentationobject can still have its internaldisplayImplswapped out for a different class if our design allowed it. - Customization: We demonstrated the power to override a delegated method (
display()) to inject custom logic (like logging) while still relying on the delegated object for the core behavior.
When to Use Class Delegation (and When Not To)
Use it when:
- You want to add new behavior to a class that already implements an interface (a clean form of the Decorator Pattern).
- You need to combine multiple behaviors from different interfaces into a single class.
- You want to avoid deeply nested inheritance hierarchies and promote a flatter, more modular design.
Be mindful when:
- Delegate Lacks Parent Awareness: Delegation is simple forwarding. The delegated object (e.g.,
displayImpl) has no automatic reference back to the delegating object (e.g.,AdvancedDocumentDelegated). This preserves loose coupling. If a back-reference is truly needed, you must pass it explicitly in the constructor. - Mutating the Delegate: Delegation is intended to be configured at construction time (
val). While declaring the delegate as a mutable property (var) is technically possible, allowing the delegate to be changed after construction is strongly discouraged. It can lead to confusion and thread-safety issues because the compiler generates code expecting a stable reference.
Frequently Asked Questions (FAQs)
Is Kotlin’s class delegation the same as the Decorator pattern?
Class delegation is widely considered a language-level syntactic sugar for a clean implementation of the Decorator Pattern. It simplifies the process of wrapping an object to enhance its functionality by eliminating the need to manually implement every single interface method.
Can I override methods that are being delegated?
Yes! As shown above, you can override any method defined in the delegated interface. Your custom implementation will always take precedence over the automatically generated forwarding call.
Does class delegation work with abstract classes?
No. Kotlin’s by delegation only works with interfaces, not abstract classes. For abstract classes, you must use traditional class inheritance.
What if two delegated interfaces have methods with the same name and signature?
This creates ambiguity. Kotlin will not compile and will force you to explicitly override that conflicting method in the delegating class. Within your override, you must then manually resolve the conflict by deciding which (or both) of the delegated implementations to call.
Your Thoughts?
What are your experiences with composition and inheritance? Have you tried Kotlin’s class delegation in your production code? How has it impacted your code quality and maintainability? Share your insights in the comments below!
If you find my content useful, feel free to support my work here. Every contribution is greatly appreciated! 🙏

Comments
Post a Comment