Using Composition in Kotlin
Learn how composition makes your Kotlin code more extensible and easy to maintain. By Prashant Barahi.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Using Composition in Kotlin
30 mins
- Getting Started
- Inheritance
- Liskov Substitution Principle
- Implementation Inheritance Antipatterns
- Single Implementation Inheritance
- Tight Coupling
- Exposing Superclass APIs Unnecessarily
- Exploding Numbers of Subclasses
- Composition
- Refactoring Using Composition
- Refactoring the UserMediator Class
- From Composition to Aggregation
- Refactoring the Pizza Class
- Handling the Exposure Problem
- Using Inheritance
- Delegation Pattern in Kotlin
- Where to Go From Here?
Using Inheritance
By now, you must be asking, “Why not go with a composition-based approach every time?” Using composition is always an option. Any class that can be implemented via inheritance can alternatively be implemented using composition. But there are cases where using inheritance proves to be more beneficial.
Inheritance is a powerful concept. And you can see it used in many places. For instance, in Android SDK, TextView
extends View
. To create a customized version of TextView
, you create a class that extends TextView
and expose additional methods or modify certain behaviors. Because both classes exhibit an “is-a” relationship with View
, they can be passed wherever View
is expected (remember substitutability?). This kind of substitution isn’t possible through a simple composition-based approach. Hence, PropertiesStore
isn’t substitutable in place of Hashtable
like Properties
is.
Unlike with implementation inheritance, composition doesn’t provide automatic delegation. Instead, you have to explicitly tell it how to interact with its instance fields by invoking their corresponding methods. If you want polymorphic behavior from a composed class, you need to use it with interfaces and write a lot of delegation or forwarding calls.
This implies that the methods provided by individual components might have to be implemented in the composed class, even if they’re only forwarding methods. In contrast, with implementation inheritance, you only need to override the methods having different behavior than the methods of the base class.
Which approach to favor depends upon the nature of the problem you’re trying to solve. Implementation inheritance isn’t bad all the time — only when you use it as a solution to the wrong problem. That’s when it backfires — sometimes to a point where classes are tightly coupled and maintenance becomes difficult.
In the next section, you’ll see how you can avoid writing delegation boilerplate with Kotlin.
Delegation Pattern in Kotlin
As you learned earlier, composition using interfaces and forwarding calls helps you get polymorphic behavior out of composed classes. But it requires writing forwarding methods. Kotlin provides a way to avoid this type of delegation boilerplate. But first, you’ll see how it’s done in a vanilla way.
Create a DelegationDemo.kt file and paste this snippet:
data class Result<T>(val item: T)
// 1
interface CanCook<T> {
fun cook(item: T): Result<T>
}
// 2
class PizzaCookingTrait : CanCook<Pizza> {
override fun cook(item: Pizza): Result<Pizza> {
println("Collecting ingredients")
item.prepare()
return Result(item)
}
}
// 3
class Chef<T>(private val trait: CanCook<T>) : CanCook<T> {
override fun cook(item: T): Result<T> {
return trait.cook(item)
}
}
fun main() {
val pizza = Pizza(PizzaType.Cheese("Mozzarella"), Size.LARGE)
val chef = Chef(trait = PizzaCookingTrait())
chef.cook(pizza)
}
Here’s a breakdown of this code:
-
CanCook<T>
is an interface that must be implemented by any class that cancook()
an item of typeT
. -
PizzaCookingTrait
‘scook()
can take in aPizza
and return a finalized tastyResult<Pizza>
. -
Chef
consists of an instance fieldtrait
and also implementsCanCook
. The overridden method delegates its functionality tocook()
of its instance field.
You can avoid writing such forwarding calls using Kotlin’s by
keyword followed by the instance to be delegated to as shown below:
class Chef<T>(private val trait: CanCook<T>) : CanCook<T> by trait
Now, from the menu, go to View > Show Bytecode and tap the Decompile button. You’ll get the equivalent Java code. Now, navigate to Chef
in that Java file and you’ll see something like this:
import kotlin.jvm.internal.Intrinsics;
// ...
public final class Chef implements CanCook {
private final CanCook trait;
public Chef(@NotNull CanCook trait) {
Intrinsics.checkNotNullParameter(trait, "trait");
super();
this.trait = trait;
}
@NotNull
public Result cook(Object item) {
return this.trait.cook(item);
}
}
Take a few moments to compare the code above with the vanilla implementation of Chef
. You can see they’re similar. So by
is a syntactical sugar for delegation code.
Where to Go From Here?
Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
Great job on making it all the way through! You’ve learned about two different ways of establishing a relationship between classes. A composition-based approach allows you to build a small, self-contained class that can be combined with other classes to build a highly encapsulated, easily testable, modular class. An inheritance-based approach takes advantage of the “is-a” relationship between classes to provide a high degree of code reuse and powerful delegation. However, you should only use an inheritance-based approach if the subtypes fulfill the “is-a” condition.
Check out Massimo Carli’s UML for Android Engineers to learn how to express relationships using diagrams.
Last, code deliberately!
If you have any questions or comments, please join the forum discussion below!