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

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.

List of APIs exposed by Properties

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.

Multilevel Inheritance

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:
Pizza Class Diagram

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.