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?
Tight Coupling
Implementation Inheritance creates a strong relationship between a parent and its subclasses. Inheriting a class ties the child class to the implementation details of the parent class. Hence, if the parent class changes — in other words if it’s unstable — the child class might malfunction even though its code hasn’t changed. As a result, every child class must evolve with its parent class.
This requires you to make a broad assumption about the future requirements. You need to build the hierarchy early and make sure the relationship remains intact with every new requirement. So you might have to go with a BDUF (Big Design Up Front) Approach, leading to over-engineering and complex design.
In the upcoming section, you’ll see how implementation inheritance breaks encapsulation.
Exposing Superclass APIs Unnecessarily
Implementation inheritance is appropriate only in circumstances where the subclass is really a subtype of the superclass. In other words, a class B
should extend a class A
only if an “is-a” relationship exists between them. Otherwise, you needlessly expose the implementation details of the superclass to the user. This opens possibilities for the clients of your class to violate its internal invariants by modifying the superclass directly.
Look at ExposureDemo.kt, located inside the exposuredemo package. The variable properties
is an instance of Properties
from the java.util package. It inherits from concrete Hashtable
. This means you can also access the public fields and methods of Hashtable
, such as put()
and get()
, through the instance of Properties
along with those of its own.
To get an idea of the APIs exposed by Properties
, go to Properties.java (located in java.util) in your IDE and click the Structure tab. You’ll see the structure of Properties
on a side panel.
Now, using the icons at the top of the panel, deselect “Show non-public” and select “Show inherited”. You’ll see something like the image above. The light grayish methods are the inherited public methods you can use via an instance of Properties
.
// [Properties] class extends from Hashtable. So, the methods from Hashtable can also be used.
val properties = Properties()
// Using [Hashtable]'s methods
properties.put("put1", "val1")
properties.put("put2", 100)
// Using [Properties]'s methods
properties.setProperty("setProperty1", "val1")
properties.setProperty("setProperty2", "100")
But there’s a catch. If you look at the documentation for Properties
, it explicitly discourages the use of Hashtable
‘s methods even though it exposes them.
// Note: [Properties] 'getProperty()' returns null if the type is not a String;
// However, [Hashtable] 'get()' returns the correct value
properties.propertyNames().toList().forEach {
println("Using Hashtable's get() $it: ${properties.get(it)}")
println("Using Properties' getProperty() $it : ${properties.getProperty(it.toString())}")
println()
}
getProperty()
of Property
has additional safety checks that get()
of Hashtable
doesn’t. The users of Properties
could bypass these checks and read directly from Hashtable
. That’s why when you run the file, you see the output shown below in the console:
Using Hashtable's get() setProperty2: 100 Using Properties' getProperty() setProperty2 : 100 Using Hashtable's get() setProperty1: val1 Using Properties' getProperty() setProperty1 : val1 Using Hashtable's get() put2: 100 Using Properties' getProperty() put2 : null Using Hashtable's get() put1: val1 Using Properties' getProperty() put1 : val1
In cases when the value is not of type String
, getProperty()
and get()
in the snippet above output different results for the same key. Therefore, the resulting API is confusing and prone to faulty invocations.
Next, you’ll learn how multilevel inheritance can cause subclasses to explode in numbers.
Exploding Numbers of Subclasses
Kotlin doesn’t support multiple inheritance. But it does support multilevel inheritance, which is used commonly. For instance, Android SDK provides a TextView
that inherits from View
. Now, to make TextView
support HTML, you can create a HtmlTextView
that inherits from TextView
. This is what a multilevel inheritance looks like.
Recall the Pizza
example in previous section. It considers only one dimension — the type of pizza (Veggie and Cheese), which was the client’s requirement when the code was written. Later, the client wants to introduce pizzas of different sizes — small, medium and large. That means you now have two independent dimensions to consider — the pizza’s type and size.
Because it doesn’t make sense for a pizza to exist without a size, you decide to make CheesePizza
and VeggiePizza
abstract. Then, you decide to extend them to account for sizes by creating three concrete implementations of each pizza type. So to accommodate the new requirement, you refactor the code as below:
abstract class Pizza {
abstract fun prepare()
}
abstract class CheesePizza : Pizza()
abstract class VeggiePizza : Pizza()
class SmallCheesePizza : CheesePizza() {
override fun prepare() {
println("Prepared a small cheese pizza")
}
}
class MediumCheesePizza : CheesePizza() {
override fun prepare() {
println("Prepared a medium cheese pizza")
}
}
class LargeCheesePizza : CheesePizza() {
override fun prepare() {
println("Prepared a large cheese pizza")
}
}
class SmallVeggiePizza : VeggiePizza() {
override fun prepare() {
println("Prepared a small veggie pizza")
}
}
class MediumVeggiePizza : VeggiePizza() {
override fun prepare() {
println("Prepared a medium veggie pizza")
}
}
class LargeVeggiePizza : VeggiePizza() {
override fun prepare() {
println("Prepared a large veggie pizza")
}
}
You can express the relationship above in form of class diagram as:
You can see the problem with this implementation. With just three sizes and two types of pizzas, you get 3*2 subclasses. Introducing a new type for any of the dimensions would significantly increase the number of subclasses. Moreover, if you change the signature of CheesePizza
to take in a cheeseName
in its constructor, the change ripples out to all the subclasses.
So, how do you deal with all these issues? Through composition!
Composition
Composition is a technique in you compose a class by adding private fields to the class that references an instance of the existing class rather than extend it. So a “has-a” relationship is established between the composed class and its contained instances. The class accomplishes its responsibility by forwarding to or invoking non-private methods of its private fields.
Using composition-based approach, you can rewrite the UserMediator
as shown below:
class UserMediator {
private val cacheService: UserCacheService = UserCacheService()
private val apiService: UserApiService = UserApiService()
// ...
}
Notice how private instance of the UserCacheService
and the UserApiService
are being used to compose UserMediator
.
Now that you have a basic understanding of composition, it’s time to see how you can use it to solve design issues introduced by implementation inheritance.