Using Composition in Kotlin

Learn how composition makes your Kotlin code more extensible and easy to maintain. By Prashant Barahi.

Leave a rating/review
Download materials
Save for later
Share

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.

Note: This article assumes you’re familiar with the basics of Kotlin. If you’re new to Kotlin, look at Programming in Kotlin before you start.

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.

List and grouping of package contents

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()
  }
}

Pizza Class Diagram

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?

  1. You have an abstract class, Pizza, with a prepare().
  2. CheesePizza and VeggiePizza are child classes of Pizza.
  3. Because child class is a parent class, you can use a CheesePizza or a VeggiePizza in any place where you need a Pizza.
  4. Even when cheesePizza and veggiePizza types are being casted to Pizza, the prepare() invokes the implementation provided by the respective child class, showing a polymorphic behavior. This is because the Pizza defines the operation you can invoke whereas the referenced object defines the actual implementation.
Note: When one class extends another class, it is called implementation inheritance. Another form of inheritance is interface inheritance, in which an interface or a class extends or implements another interface. Because interfaces have no implementation details tied, interface inheritance doesn’t have the same problem as implementation inheritance.

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.