Decorator pattern in Kotlin – Design Patterns

Nowadays, optionality is everywhere. Typically, before purchasing an item, one can choose among multiple configurations, models, and kinds available.

The decorator pattern will allow you to implement a dynamic code structure where the new behaviors can be applied to the basic object.

It increases your optionality.

Pattern introduction

The decorator pattern allows you to apply new behavior to the objects in a dynamic way.

Let’s rephrase the definition of this pattern, based on the famous GoF book:

Decorator pattern – allows to dynamically apply new behavior to the object. Decorators give you flexibility similar to inheritance, offering in return a much more extended functionality.

A decorator is a serious alternative for inheritance. With the composition and delegation, it allows adding new responsibilities in a runtime.

The decorator is widely used in Java standard library, you can find it in the java.io streams (InputStream, OutputStream, Reader, Writer, etc.) and in java.util.Collections.

Open-closed principle

The decorator pattern is following the open-closed principle (OCP). It’s one of the good design rules in object-oriented programming.

Let’s rephrase its definition.

Classes should be open for extension but closed for modifications.

(Open-Closed Principle)

What does it exactly mean in our case? We’re going to use composition instead of inheritance.

Extending an object’s behavior can be done in runtime (instead of statically setting the behavior as it is for the inheritance). The original code of the parent class will remain intact but the expected outcome will change. No parent class modification means that there will be no room for new bugs introduction or possible side-effects.

Is that definition inconsistent? Of course, not. There are OOP techniques that allow us to extend the existing functionalities without the direct modification of code

Remember what the Observer pattern has allowed us to do? By adding new observer objects we could extend the observable object without the need to add new code to the observable class.

Please note that it’s not a golden rule. Following OCP everywhere can lead to complex and hard-to-follow code creation.

Example – coffee shop

Let’s take a coffee shop as an example for the demonstration of the decorator pattern.

(Non) caffeinated drinks are typically available in different configurations – with or without additives.

Our goal is to provide the functionality that allows us to order the drinks. It should calculate the overall cost of the drink and provide a meaningful description of the drink with its all additives.

Classes

Let’s take a look at the classes we’re going to use in this example:

package kt.design.patterns.decorator


internal abstract class Drink(open val description: String = "Unknown") {
    abstract fun cost(): Double
}

internal abstract class AdditiveDecorator : Drink()

internal class Coffee : Drink(description = "Black coffee") {
    override fun cost() = 1.50
}

internal class NonCaffeine : Drink(description = "Non caffeinated coffee") {
    override fun cost() = 2.50
}

internal class Milk(private val drink: Drink) : AdditiveDecorator() {
    override val description = "${drink.description}, Milk"
    override fun cost() = drink.cost() + 0.20

    companion object {
        fun withDrink(newDrink: Drink) = Milk(newDrink)
    }
}

internal class Cinnamon(private val drink: Drink) : AdditiveDecorator() {
    override val description = "${drink.description}, Cinnamon"
    override fun cost() = drink.cost() + 0.20

    companion object {
        fun withDrink(newDrink: Drink) = Cinnamon(newDrink)
    }
}

As we can see, we have:

  • Drink – this is the component abstract class interface. It has the cost method which will be used to cost calculation and the description property. It has the following subclasses:
    • Coffee, NonCaffeine – implementing respective methods setting fixed price and descriptions.
  • AdditiveDecorator – this is the decorator abstract class that inherits the Drink class. It has the following subclasses:
    • Milk, Cinnamon – similarly to Coffee and NonCaffeine, additives are also overriding the methods adding their own costs at the top of the passed newDrink drink cost.
      Additionally, they have withDrink factory methods that will help us increase the readability of code. See them in action in the test cases below.

The decorator in action – test

The mechanics of the decorator pattern can be easily seen in action in the example below.

Let’s take a look at the simple test cases:

package kt.design.patterns.decorator

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test

internal class DrinksTest {

    @Test
    fun `should serve drinks with additives and return a valid cost`() {
        val noCafWithCinnamon = Cinnamon.withDrink(NonCaffeine())
        val coffeeWithMilk = Milk.withDrink(Coffee())

        assertEquals(2.7, noCafWithCinnamon.cost())
        assertEquals(1.7, coffeeWithMilk.cost())
    }

    @Test
    fun `should check costs and descriptions of the coffee and the coffee with additives`() {
        val coffee = Coffee()
        val coffeeWithMilk = Milk.withDrink(coffee)
        val coffeeWithMilkAndCinnamon = Cinnamon.withDrink(coffeeWithMilk)

        assertEquals(coffee.description, "Black coffee")
        assertEquals(coffeeWithMilkAndCinnamon.description, "Black coffee, Milk, Cinnamon")
        assertEquals(1.5, coffee.cost())
        assertEquals(1.9, coffeeWithMilkAndCinnamon.cost())
    }

}

Introduction of withDrink factory methods are not a part of a standard decorator pattern implementation but it allows us to increase the readability of the code.

The standard, wrapping approach to decorated object creation (like Cinnamon(Milk(Coffee())))) is not very elegant. Imagine creating very complex multiple decorated objects this way.

As always you can try the code yourself, it’s available on GitHub.

Conclusion

If you’re looking for a way to create flexible and configurable objects then the decorator pattern is your way to go.

Use it carefully though because overusing it can increase the overall complexity of the code (too many wrappers and small classes in the codebase).

Resources

Leave a Reply

Your email address will not be published.