Decorator Design Pattern in Kotlin

1. Definition

The Decorator Design Pattern is a structural design pattern that allows you to add new functionalities to existing objects dynamically without altering its structure. It involves a set of decorator classes that mirror the type of the objects they enhance, but add or override behavior.

2. Problem Statement

Consider a simple coffee shop application. You have different beverages like espresso, latte, etc. Now, you want to add condiments/add-ons like milk, sugar, chocolate, etc. The challenge is, how can you do this without creating a multitude of subclasses for each possible combination? Or without modifying existing code whenever you want to add a new condiment?

3. Solution

The Decorator Pattern offers a flexible solution. Instead of creating a new subclass for each combination, you create separate decorators for each condiment. These decorators wrap the original object (or another decorator) and add their own behavior (like cost and description).

4. Real-World Use Cases

1. Java's InputStream, which has decorators like BufferedInputStream, DataInputStream, etc.

2. GUI toolkits where you can dynamically add behaviors or responsibilities to individual UI components.

3. Middleware in web servers, adding functionalities like logging, caching, etc.

5. Implementation Steps

1. Define a common interface for the original objects and decorators.

2. Create concrete components.

3. Create decorator classes that wrap concrete components or other decorators.

6. Implementation in Kotlin

// Step 1: Common Interface
interface Beverage {
    fun cost(): Double
    fun description(): String
}

// Step 2: Concrete Components
class Espresso : Beverage {
    override fun cost() = 1.99
    override fun description() = "Espresso"
}

// Step 3: Decorators
class Milk(private val beverage: Beverage) : Beverage {
    override fun cost() = beverage.cost() + 0.30
    override fun description() = "${beverage.description()}, Milk"
}

class Sugar(private val beverage: Beverage) : Beverage {
    override fun cost() = beverage.cost() + 0.20
    override fun description() = "${beverage.description()}, Sugar"
}

fun main() {
    val espressoWithMilkAndSugar = Sugar(Milk(Espresso()))
    println("${espressoWithMilkAndSugar.description()} costs ${espressoWithMilkAndSugar.cost()}")
}

Output:

Espresso, Milk, Sugar costs 2.49

Explanation:

1. A common interface Beverage is defined, which provides methods for cost and description.

2. The concrete component Espresso implements this interface providing its cost and description.

3. Decorator classes Milk and Sugar also implement the Beverage interface. They take a Beverage object as a parameter, which can be a concrete beverage or another decorator. These decorators add or modify the behavior of the cost and description methods.

4. In the main function, we create an espresso beverage and decorate it with milk and sugar. We demonstrate how decorators can be layered dynamically to add multiple functionalities.

7. When to use?

Use the Decorator pattern when:

1. You want to add responsibilities to individual objects, not to an entire class.

2. You want to add responsibilities to objects dynamically and remove them as well.

3. Extending functionalities by subclassing is impractical.

By leveraging the Decorator pattern, you can ensure flexibility and adherence to the Open/Closed Principle: classes should be open for extension but closed for modification.


Comments