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
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

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:

  1. CanCook<T> is an interface that must be implemented by any class that can cook() an item of type T.
  2. PizzaCookingTrait‘s cook() can take in a Pizza and return a finalized tasty Result<Pizza>.
  3. Chef consists of an instance field trait and also implements CanCook. The overridden method delegates its functionality to cook() 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!