14

Using Composition in Kotlin

 2 years ago
source link: https://www.raywenderlich.com/21964446-using-composition-in-kotlin
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Home Android & Kotlin Tutorials

Using Composition in Kotlin

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

By Prashant Barahi Jun 28 2021 · Article (30 mins) · Intermediate

5/5 1 Rating

Version

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.

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?

  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.

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.

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.

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:

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.

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

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.

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.

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!

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.

Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!

Average Rating

5/5

Add a rating for this content

Sign in to add a rating
1 rating

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK