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

Refactoring Using Composition

With composition, you use small parts to compose a complex object. In this section, you’ll see how you can use this composition-based approach to avoid or mitigate the design issues introduced by implementation inheritance.

Refactoring the UserMediator Class

Because you can’t extend more than one parent class, the simplest fix to the broken UserMediator would be to remove the open keyword from UserApiService and UserCacheService, and instead have them as private instance fields of UserMediator, as shown in the snippet below:

class UserMediator {

  private val cacheService: UserCacheService = UserCacheService()
  private val apiService: UserApiService = UserApiService()

  /**
   * Search for [User] with [username] on cache first. If not found,
   * make API calls to fetch the [User] and persist it in server.
   *
   * @throws UserNotFoundException if it is not in the "server".
   */
  fun fetchUser(username: String): User {
    return cacheService.findUserById(username)
      ?: apiService.fetchUserByUsername(username)?.also { cacheService.saveUser(it) }
      ?: throw UserNotFoundException(username)
  }
}

UserMediator Class Diagram

Notice how the classes that UserMediator extended are converted to private instance fields of that class.

Moreover, you can make this class easier to test by accepting these instance fields as arguments to the constructor and outsourcing the creation of these fields to the client. This is called dependency injection.

From Composition to Aggregation

Remove these two fields and create a constructor for UserMediator, taking these two instance variables as arguments:

class UserMediator(
  private val cacheService: UserCacheService,
  private val apiService: UserApiService
) {
   // methods...
}

And in main() of the UserDemo.kt, use the following code to initialize mediator:

val mediator = UserMediator(
  cacheService = UserCacheServiceImpl(),
  apiService = UserApiServiceImpl()
)

UserMediator now depends on the user of the class to provide its dependencies. And during testing, you can pass in test stubs that fit your test situation — making testing a lot easier.

Note: Composition is a strong form of association whereas a weaker form of association is called aggregation. The first revision of UserMediator (the one with default constructor) exhibits composition relationship with its contained instances since their lifecycle is bound to the container’s lifecycle. In contrast, the second revision of UserMediator (the one with constructor arguments) exhibits aggregation relationship since it expects client to supply the dependencies. So the contained instances can exists even after the object of UserMediator is destroyed.

UserMediator as Aggregation

Put your caret on the UserMediator class definition and press Control-Enter. Then, select Create test. This creates a file — UserMediatorTest.kt — inside the test directory. Open it and paste the following snippet:

internal class UserMediatorTest {
  private lateinit var mockApi: UserApiService
  private lateinit var realCache: UserCacheService

  @BeforeEach
  fun setup() {
    // 1
    realCache = UserCacheServiceImpl()

    // 2
    mockApi = object : UserApiService {
      private val db = mutableListOf<User>()

      init {
        db.add(User("testuser1", "Test User"))
      }

      override fun fetchUserByUsername(username: String): User? {
        return db.find { username == it.username }
      }
    }
  }

  @Test
  fun `Given username when fetchUser then should return user from cache and save it in cache`() {
    // 3
    val mediator = UserMediator(realCache, mockApi)
    val inputUsername = "testuser1"
    val user = mediator.fetchUser(inputUsername)
    assertNotNull(user)
    assertTrue { user.username == inputUsername }
    // Check if saved in cache
    assertNotNull(realCache.findUserById(inputUsername))
  }
}

Here’s a breakdown of the code above:

  1. Initialize realCache as an instance of UserCacheServiceImpl. Because this class only uses in-memory data structure, you don’t have to mock it.
  2. But UserApiServiceImpl performs a “network” call, and you don’t want the result of test cases to depend on the server’s response or availability. So it’s better to mock or stub it. Here, you’ve replaced it with an implementation that instead uses in-memory data structure, so you determine its result and can change it to match your test scenario.
  3. Because UserMediator takes instances of UserCacheService and UserApiService as arguments, you can pass in the above variables.

In the next section, you’ll refactor the exploded subclasses using a composition-based approach.

Refactoring the Pizza Class

Previously, you saw how multilevel inheritance can cause the number of subclasses to explode in number. You can avoid this problem by not modeling the relationship in the form of multilevel inheritance and instead establishing a “has-a” relation between Pizza and the dimensions.

Open the Pizza.kt file inside the explosionofsubclassesdemo package and replace the content with the following snippet:

import java.math.RoundingMode

// 1
sealed class PizzaType {
  data class Cheese(val cheeseName: String) : PizzaType()
  data class Veggie(val vegetables: List<String>) : PizzaType()
}

enum class Size(val value: Int) {
  LARGE(12), MED(8), SMALL(6);

  fun calculateArea(): Double {
    // Area of circle given diameter
    return (Math.PI / 4).toBigDecimal().setScale(2, RoundingMode.UP).toDouble() * value * value
  }
}

// 2
class Pizza(val type: PizzaType, val size: Size) {
  fun prepare() {
    // 3 
    println("Prepared ${size.name} sized $type pizza of area ${size.calculateArea()}")
  }
}

Here’s what’s going on in the snippet above. You:

  1. Extract the dimensions into a separate class — PizzaType — and Size.
  2. Have the original class refer to an instance of the extracted class. Here, the class Pizza consists of its two-dimension classes.
  3. Make the composed class delegate any size-related calculation to the Size class or any type-related calculation to the PizzaType class. This is how the composed class fulfills its responsibility: by interacting with the instance fields.

Finally, to run the class, open PizzaDemo.kt and replace the code in main() with:

val largeCheesePizza = Pizza(Cheese("Mozzarella"), Size.LARGE)
val smallVeggiePizza = Pizza(Veggie(listOf("Spinach", "Onion")), Size.SMALL)
val orders = listOf(largeCheesePizza, smallVeggiePizza)

orders.forEach {
  it.prepare()
}

Finally, run the file and you get the following output:

Prepared LARGE sized Cheese(cheeseName=Mozzarella) pizza of area 113.76
Prepared SMALL sized Veggie(vegetables=[Spinach, Onion]) pizza of area 28.44

With this implementation, you can add to any of the dimensions without having to worry about the explosion of subclasses.

Next, you’ll see how you can use a composition-based approach to control the exposure of APIs.

Handling the Exposure Problem

The rule of thumb in OOP is to write shy class. Shy class doesn’t reveal unnecessary implementation about itself to others. The java.util‘s Properties clearly violate this. A better way to implement it would have been to use a composition-based approach instead.

Because Properties is a built-in class provided by JDK, you won’t be able to modify it. So you’ll learn how it could have been made better, using a simplified version of it as an example. For this, create a new HashtableStore class and paste the following snippet:

class HashtableStore {
  // 1
  private val store: Hashtable<String, String> = Hashtable()

  // 2
  fun getProperty(key: String): String? {
    return store[key]
  }

  fun setProperty(key: String, value: String) {
    store[key] = value
  }

  fun propertyNames() = store.keys
}

Here’s the code breakdown:

  1. With a composition-based approach, you create a private field in HashtableStore and initialize it as an instance of Hashtable. To provide the functionality of data storage, you need to interact with this instance. Recall the rule of thumb: Write shy classes. Making the instance private prevents outsiders from accessing it, helping you achieve encapsulation!
  2. You expose public methods that the user of this class can access. This class exposes three such methods, and each method forwards its operation to the private field.

In the same file, create main() and paste the following code inside it:

val properties = HashtableStore()

properties.setProperty("setProperty1", "val1")
properties.setProperty("setProperty2", "100")

properties.propertyNames().toList().forEach {
  println("$it: ${properties.getProperty(it.toString())}")
}

If you want all the features Properties provides while keeping the “exposure area” under your control, you can create a wrapper around it and expose your own methods. Create a new class PropertiesStore and paste in the following code:

class PropertiesStore {
  private val properties = Properties()

  fun getProperty(key: String): String? {
    return properties.getProperty(key)
  }

  fun setProperty(key: String, value: String) {
    properties.setProperty(key, value)
  }

  fun propertyNames() = properties.propertyNames()
}

Like HashtableStore, PropertiesStore uses a private instance but of Properties with public methods that interact with it. Because you use Properties as an instance field, you also get the benefits from any future updates on Properties.

You’ve learned how a composition-based approach can help you solve design issues. In the next section, you’ll learn about its shortcomings.