Favoring Composition Over Inheritance with Kotlin Delegates

An extensive guide on Kotlin Delegates

Composition Over Inheritance is an important design principle of Object Oriented Programming. It makes the code more reusable and maintainable. This is the often-stated principle of OOP, such as in the influential book Design Patterns: Elements of Reusable Object-Oriented Software.

Let’s see what Wikipedia says:

Composition over inheritance is the principle that classes should achieve polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) rather than inheritance from a base or parent class. — Wikipedia

Favoring Composition Over Inheritance: Kotlin’s “by” magic
Photo by Vardan Papikyan on Unsplash

In this article, we will see how we can make use of Kotlin Delegates to implement composition

What’s the problem with Inheritance?

Inheritance is a powerful feature, but it is designed to create a hierarchy of objects with an “is a” relationship. When such a relationship is not clear, inheritance might be problematic and should be implemented cautiously. These are the common issues with the Inheritance implementation:

  1. Inheritance creates a strong relationship between a parent and its subclasses and results in tightly coupled code. Inheriting a class ties the child class to the implementation details of the parent class so when the code in the parent class changes all the child classes might need to update.
  2. The use of a parent class just to keep all the common pieces of code breaks the single responsibility principle and eventually results in Spaghetti code
  3. Implementing inheritance, to extend the class’ functionality, is not possible when classes are closed or final

When all we need is a simple code extraction or reuse, inheritance should be used with caution; instead, we should prefer a lighter alternative: class composition.

Implementing Composition

Making Composition as powerful for code reuse as inheritance is possible with Delegation. Delegation essentially means transferring or forwarding the request to the concerned delegate object. Two objects under Delegation deal with a request: a receiving object delegates tasks to its delegate, who then handles the request.

The Delegation Pattern is a popular design pattern where a parent object will pass on requests to a child a Delegate Object. This provides code re-use that is similarly achieved via inheritance while also enforcing the “Single Responsibility Principle” which allows the parent to remain agnostic of how the request is performed.

The Delegation pattern has proven to be a good alternative to implementation inheritance, and Kotlin supports it natively requiring zero boilerplate code.

What Is Kotlin Delegation?

Kotlin has built-in support for delegates and a new keyword called “by” is added to support the “delegation” design pattern. It can be used for property delegation or implementation by delegation.

“by” can be used for both interface implementation by delegation and property delegation. We will have a look at both of these below.

Interface implementation by delegation

Have a look at the code below:

interface BaseCar {
    fun color()
    fun maxSpeed()
}

class BaseCarImpl(val color: String) : BaseCar {
    override fun color() { print(color) }
    override fun maxSpeed() { print("250") }
}

class Derived(b: BaseCar) : BaseCar by b

fun main() {
    val b = BaseCarImpl("Green")
    Derived(b).color()
}

In the example above, the class Derived implements the interface BaseCar but it does not need to override any of the interface’s methods. Derived class just delegates the incoming request to the actual implementation which is BaseCarImpl in this case. If we see the output of the code above, we will see “Green” printed in the console

We can further customize the behavior by overriding the required methods in the Derived class. Check the code below:

class Derived(b: BaseCar) : BaseCar by b{
    override fun color() { print("Red") }
}

fun main() {
    val b = BaseCarImpl("Green")
    Derived(b).color()
}

In this case, we will “Red” as output in the console. Derived class won’t delegate the request to the delegate object as we have overridden the behavior of the method color() in theDerived class.

This delegate implementation is especially useful in Android’s BaseActivity class which in general is created to keep the common piece of code. Using BaseActivity just to hold the common piece of code breaks the single responsibility principle and also we end up exposing the APIs from the parent class which all the child classes may not even need.

Have a look at a more realistic example:

Consider we have 2 types of user tiers defined for our product: A Free Tier and a Paid Tier. So to handle this on the app side we can have an Interface called Tier and the corresponding Implementation for the Paid and Free Tier: FreeTierImpl and PaidTierImpl . Our final code will look something like this:

interface Tier {
    fun getLicenseType() : Int
    fun numberOfFeatureAvailable() : Int
}

class FreeTierImpl() : Tier {
    override fun getLicenseType() : Int { return 1 }
    override fun numberOfFeatureAvailable() : Int { return 2 }
}

class PaidTierImpl() : Tier {
    override fun getLicenseType() : Int { return 2 }
    override fun numberOfFeatureAvailable() : Int { return 4 }
}

class CompositeService(val tier: Tier, val messagingServ: MessagingService) : Tier by tier
, MessagingService by messagingServ{
    
}

fun main() {
    val service = CompositeService(PaidTierImpl(), UserMessagingSErvice())
    service.numberOfFeatureAvailable()
}

CompositeService just aggregates the two services Tier and MessagingService but it remains agnostic of the business logic implemented for calculating the license of the number of features. Arguments in the constructor of CompositeService class can be injected via some DI logic

In the next section, we will look into property delegation

Property delegation

With some common kinds of properties, even though we can implement them manually every time we need them, it is more helpful to implement them once, add them to a library, and reuse them later. We can create our own custom property delegates but for the brevity of this article, we will look into some standard delegates that Kotlin provides out of the box. The Kotlin standard library provides factory methods for several useful kinds of delegates.

1. Lazy property

lazy() is a function that takes a lambda and returns an instance of Lazy<T>, which can serve as a delegate for implementing a lazy property. The first call to get() executes the lambda passed to lazy() and remembers the result. Subsequent calls to get() simply return the remembered result. This can be useful for properties that might be expensive to compute and that we might not ever need. Check the example below:

class UserDb(userId: String) {
    val name: String by lazy {
        queryForValue("SELECT name FROM users_table WHERE id = :id", mapOf("id" to userId)
    }
}

2. Delegates.observables()

Delegates.observable() takes two arguments: the initial value and a handler for modifications.

The handler is called every time we alter the property (after the assignment has been performed). Lambda has three parameters: the property being assigned to, the old value, and the new value.

class ObservedProperty {
    var name: String by Delegates.observable("Initia value") {
        prop, old, new -> println("Old value: $old, New value: $new")
    }
}

3. Storing properties in a map

One common use case is keeping property values on a map. This commonly occurs in applications like parsing JSON or carrying out other dynamic operations. Here, the map instance itself can serve as the delegate for a delegated property. Look at the code sample below:

class DelegateMapExample(map: MutableMap<String, Any?>) {
    var name: String by map
    var license: Int by map
}

fun main() {
    val data = DelegateMapExample(mapOf(
        "name" to "USP",
        "license" to 4
    ))
    println(data.name)
}

The output of the code will be “USP”. Delegated properties use string keys associated with the names of properties to get values from this map.

Final Thoughts

Property Delegation and Implementation by Delegation are powerful features provided by Kotlin. I hope this article has motivated you to take advantage of them.

This brings us to the end of the article. I hope you find this stuff useful. And since you have reached this far in the article, add a comment below and share the article

References:

https://kotlinlang.org/docs/delegation.html

https://kotlinlang.org/docs/delegated-properties.html

Leave a comment