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?
Object-oriented programming (OOP) introduces concepts to favor code reuse and extensibility, protect from illegal mutation/states and preserve data integrity while allowing users to model entities. But if used inadvertently, these concepts that make object-oriented programming one of the most popular programming paradigms can also make the software fragile and difficult to maintain.
OOP merely provides the ingredients — it’s up to you to use those ingredients deliberately and cook good software. Remember the primary value of software is its ability to tolerate and facilitate the changes in users’ requirements throughout its life. Meeting the users’ current requirements effectively is its secondary value. Thus, organizing your classes so your software provides value to the user and continues to do so is a must.
Inheritance and composition are techniques you use to establish relationships between classes and objects. It’s important to understand which of them to favor to achieve a good software design.
In this tutorial, you’ll:
- Understand inheritance and composition.
- Use an inheritance-based approach to write classes and learn about their shortcomings.
- Learn about delegation patterns.
- Use composition to refactor inheritance-based classes.
- Learn about Kotlin’s
by
keyword.
In the process, you’ll go through different example classes and learn a better way to implement them.
Now, it’s time to get cooking.
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Fire up IntelliJ IDEA and select Open…. Then, navigate to and open the starter project’s folder.
You’ll see classes grouped in packages. For convenience, these packages have names based on the sections of this article.
Each package contains *Demo.kt
files, which house a main()
. The most important thing to note is that the starter project contains a lot of badly designed classes, so don’t use it as inspiration — and you’ll be refactoring them as you follow along.
Inheritance
Inheritance establishes an “is-a” relationship between the classes. So a child class inherits every non-private field and method from its parent class. Because of this, you can substitute a child class in place of its parent.
// 1
abstract class Pizza() {
abstract fun prepare()
}
// 2
class CheesePizza() : Pizza() {
override fun prepare() {
println("Prepared a Cheese Pizza")
}
}
class VeggiePizza() : Pizza() {
override fun prepare() {
println("Prepared a Veggie Pizza")
}
}
fun main() {
// 3
val cheesePizza: Pizza = CheesePizza()
val veggiePizza: Pizza = VeggiePizza()
val menu = listOf(cheesePizza, veggiePizza)
for (pizza in menu) {
// 4
pizza.prepare()
}
}
If you build and run this file, you get the following output:
Prepared a Cheese Pizza Prepared a Veggie Pizza
So, what’s going on here?
- You have an abstract class,
Pizza
, with aprepare()
. -
CheesePizza
andVeggiePizza
are child classes ofPizza
. - Because child class is a parent class, you can use a
CheesePizza
or aVeggiePizza
in any place where you need aPizza
. - Even when
cheesePizza
andveggiePizza
types are being casted toPizza
, theprepare()
invokes the implementation provided by the respective child class, showing a polymorphic behavior. This is because thePizza
defines the operation you can invoke whereas the referenced object defines the actual implementation.
Moreover, you can override the non-final accessible methods of the parent class in its child class. But you must ensure that the overridden methods preserve the substitutability promoted by the Liskov Substitution Principle (LSP). You’ll learn about this in the next section.
Liskov Substitution Principle
The core of LSP is that the subclasses must be substitutable for their superclasses. And in order for this to happen, the contracts defined by the superclass must be fulfilled by its subclasses. Contracts like function signatures (function name, return types and arguments) are enforced as compile-time errors by statically typed languages like Java and Kotlin.
However, operations like unconditionally throwing exceptions, such as UnsupportedOperationException
, in the overridden methods when it’s not expected in superclass — violate this principle.
You can check if a method in the newly introduced or modified subclass violates LSP by seeing if the change requires every invocation of the method in hand to be wrapped with an if
statement to test whether the method in hand should be invoked or not depending on the newly introduced subclass i.e. an is
check.
Implementation Inheritance Antipatterns
Implementation inheritance serves as a powerful way to achieve code reuse, but it might not be the right tool for every scenario. Using implementation inheritance where it’s inappropriate could introduce maintenance problems. You’ll learn about these in the upcoming sections.
Single Implementation Inheritance
Java Virtual Machine languages like Kotlin and Java don’t allow a class to inherit from more than one parent class.
Expand the userservice package. It contains two service classes: UserCacheService
, which stores User
records in an in-memory data structure, and UserApiService
, which has a delay to simulate a network call. Ignore UserMediator
for now.
Suppose you have to write a class that interacts with both UserCacheService
and UserApiService
to get a User
record. You’re required to make the operation fast, so you first search the user in UserCacheService
and return if it exists. Otherwise, you need to perform a slow “network” call. When UserApiService
returns a User
, you save it in the cache for future use. Can you model this using implementation inheritance?
// Error: Only one class may appear in a supertype list
/**
* Mediates repository between cache and server.
* In case of cache hit, it returns the data from the cache;
* else it fetches the data from API and updates the cache before returning the result.
**/
class UserMediator: UserApiService(), UserCacheService() {
}
First, the code above won’t compile. And even if it did, the relationship wouldn’t make sense because rather than an is-a relationship, UserMediator
uses UserCacheService
and UserApiService
as implementation details. You’ll see how to fix this later.