Decorator Design Pattern

Manu Aravind
7 min readAug 13, 2023

--

The Decorator design pattern is a structural design pattern that allows you to add behavior or responsibilities to individual objects without modifying their code. It’s a way to extend the functionality of classes dynamically, at runtime, by wrapping them in a series of decorator objects.

This pattern is useful when you have a base class or interface and you want to add features or behavior to its instances without creating a new subclass for each combination of features. It promotes the principle of open-closed design, where classes are open for extension but closed for modification.

Advantages of decorator design pattern

  1. Open-Closed Principle: The Decorator pattern adheres to the Open-Closed Principle, one of the SOLID principles. This means that you can extend the behavior of a class without modifying its source code. New decorators can be added to provide new functionality without altering existing code.
  2. Flexible Composition: Decorators can be combined in various ways to create complex combinations of behaviors. You can wrap objects with multiple decorators to achieve layered functionality. This flexibility enables a high degree of customization while keeping the code modular and maintainable.
  3. Single Responsibility Principle: The Decorator pattern allows you to separate concerns and responsibilities by creating a set of smaller, focused decorator classes. Each decorator handles a specific behavior, making the codebase easier to understand and maintain.
  4. Incremental Building of Objects: Decorators allow you to incrementally build up an object’s functionality by adding decorators one by one. This is in contrast to subclassing, where you might need to create multiple subclasses to achieve similar behavior. With decorators, you can mix and match behaviors as needed.
  5. Reuse and Composition: By using decorators, you can reuse existing classes and behaviors, composing them in different ways to suit different requirements. This promotes code reuse and reduces the need to duplicate code.
  6. Separation of Concerns: Decorators promote separation of concerns by isolating specific behavior enhancements into separate decorator classes. This makes the codebase more organized and easier to maintain, as changes to one concern don’t necessarily impact others.
  7. Dynamic Behavior Addition: Decorators allow you to dynamically attach new behaviors to objects at runtime. This dynamic behavior addition can be useful in scenarios where you need to change or enhance an object’s functionality during program execution.
  8. Consistent Interface: Decorators adhere to the same interface as the components they decorate. This ensures that clients can interact with decorated objects in the same way they would with the original components, enhancing code consistency and readability.

Implementation Details

Key Participants in the Decorator Pattern:

  1. Component: This is the base interface or abstract class that defines the common interface for all concrete components and decorators. It usually contains the core functionality that decorators will enhance.
  2. ConcreteComponent: This is a class that implements the Component interface. It represents the basic object to which decorators can be added.
  3. Decorator: This is the abstract class that implements the Component interface and holds a reference to a Component object. It adds additional responsibilities or behavior to the component. Decorators can be layered to create a stack of functionalities.
  4. ConcreteDecorator: These are the classes that extend the Decorator class and provide specific implementations of the additional functionalities.

There are two ways to implement decorator design patterns in kotlin.

  1. By Composition: When using composition to implement the Decorator pattern, we’ll need an abstract class that will act as the composer or decorator for our target object:
  2. By Delegation: The Delegation pattern has proven to be a good alternative to implementation inheritance, and Kotlin supports it natively, requiring zero boilerplate code. This feature makes it easy to create decorators using class delegation with the use of the by keyword.

Example 1: Text Formatting Decorator

By Composition

interface Text {
fun format(): String
}

class BasicText : Text {
override fun format(): String {
return "This is a basic text."
}
}

class BoldText(private val decoratedText: Text) : Text {
override fun format(): String {
return "<b>${decoratedText.format()}</b>"
}
}

fun main() {
val basicText: Text = BasicText()
println(basicText.format())
val boldText: Text = BoldText(basicText)
println(boldText.format())
}

Output:

This is a basic text.
<b>This is a basic text.</b>

2. By Delegation

interface Text {
fun format(): String
}

class BasicText : Text {
override fun format(): String {
return "This is a basic text."
}
}

class BoldText(private val decoratedText: Text) : Text by decoratedText {
override fun format(): String {
return "<b>${decoratedText.format()}</b>"
}
}

fun main() {
val basicText: Text = BasicText()
println(basicText.format())
val boldText: Text = BoldText(basicText)
println(boldText.format())
}

Output:

This is a basic text.
<b>This is a basic text.</b>

Example 2: Coffee Shop with Condiments

1 By Composition

interface Coffee {
fun cost(): Double
}

class SimpleCoffee : Coffee {
override fun cost(): Double {
return 5.0
}
}

class MilkDecorator(private val decoratedCoffee: Coffee) : Coffee {
override fun cost(): Double {
return decoratedCoffee.cost() + 4.0
}
}

class SugarDecorator(private val decoratedCoffee: Coffee) : Coffee {
override fun cost(): Double {
return decoratedCoffee.cost() + 2.0
}
}

fun main() {
val simpleCoffee: Coffee = SimpleCoffee()
println(simpleCoffee.cost())
val milkCoffee: Coffee = MilkDecorator(simpleCoffee)
println(milkCoffee.cost()) // Output: 9.0
val sugarCoffee: Coffee = SugarDecorator(milkCoffee)
println(sugarCoffee.cost()) // Output: 11.0
}

Output:

5.0
9.0
11.0

2. By Delegation

interface Coffee {
fun cost(): Double
}

class SimpleCoffee : Coffee {
override fun cost(): Double {
return 5.0
}
}

class MilkDecorator(private val decoratedCoffee: Coffee) : Coffee by decoratedCoffee {
override fun cost(): Double {
return decoratedCoffee.cost() + 4.0
}
}

class SugarDecorator(private val decoratedCoffee: Coffee) : Coffee by decoratedCoffee {
override fun cost(): Double {
return decoratedCoffee.cost() + 2.0
}
}

fun main() {
val simpleCoffee: Coffee = SimpleCoffee()
println(simpleCoffee.cost())
val milkCoffee: Coffee = MilkDecorator(simpleCoffee)
println(milkCoffee.cost()) // Output: 7.0
val sugarCoffee: Coffee = SugarDecorator(milkCoffee)
println(sugarCoffee.cost())
}

Output:

5.0
9.0
11.0

Example 3: Logger Decorators

By composition

interface Logger {
fun log(message: String)
}

class ConsoleLogger : Logger {
override fun log(message: String) {
println("Console: $message")
}
}

class TimestampLogger(private val decoratedLogger: Logger) : Logger {
override fun log(message: String) {
val timestamp = System.currentTimeMillis()
decoratedLogger.log("[$timestamp] $message")
}
}

fun main() {
val logger: Logger = ConsoleLogger()
val timestampLogger: Logger = TimestampLogger(logger)
timestampLogger.log("Hello, world!")
}

Output:

Console: [1691918892584] Hello, world!

By Delegation

interface Logger {
fun log(message: String)
}

class ConsoleLogger : Logger {
override fun log(message: String) {
println("Console: $message")
}
}

class TimestampLogger(private val decoratedLogger: Logger) : Logger by decoratedLogger {
override fun log(message: String) {
val timestamp = System.currentTimeMillis()
decoratedLogger.log("[$timestamp] $message")
}
}

fun main() {
val logger: Logger = ConsoleLogger()
val timestampLogger: Logger = TimestampLogger(logger)
timestampLogger.log("Hello, world!")
}

Output:

Console: [1691918951678] Hello, world!

Conclusion

The Decorator design pattern is a structural pattern in software design that allows you to dynamically add or modify behavior to individual objects without affecting the behavior of other objects from the same class. It’s typically used when you want to add responsibilities to objects in a flexible and reusable way. Both composition and delegation are concepts used in implementing the Decorator pattern, but they have slightly different implementations.

Using Composition: In this approach, the Decorator pattern is implemented by creating a set of decorator classes that are composed with the original object. Each decorator adds a specific behavior or responsibility to the object. The decorators are stacked on top of each other, and they can be combined in various ways to achieve different combinations of behaviors.

Advantages:

  • Offers a high degree of flexibility, as you can mix and match decorators to achieve various combinations of behavior.
  • Allows for easy addition of new decorators without modifying existing code.

Disadvantages:

  • This can lead to a large number of decorator classes if not managed properly, which might increase complexity.
  • The stack of decorators might become difficult to manage and debug if not structured well.

Using Delegation: In this approach, the Decorator pattern is implemented by using delegation, where the original object and its decorators implement the same interface or extend the same base class. The decorators contain a reference to the original object and delegate specific responsibilities to it while adding new behavior.

Advantages:

  • Simplifies the structure by directly using the same interface or class inheritance for both the original object and its decorators.
  • Easier to understand and manage, as there’s a clear separation between the base object and its decorators.

Disadvantages:

  • This can limit the flexibility compared to the composition approach, as the decorators are tightly coupled with the original object.
  • Adding new decorators might require changes to the base object’s interface or class hierarchy.

Decorator design pattern is used to add responsibilities or behaviors to objects dynamically without altering their structure. There are two common ways to implement the Decorator pattern: using composition and using delegation. Both approaches achieve the same goal but have some differences in implementation.

Let’s take an example of a simple coffee shop scenario where we have different types of coffee and we want to add extra ingredients as decorations.

Using Composition:

  1. Direct Composition: In this approach, each decorator class has a direct composition relationship with the component interface or class. Decorators add their behavior and then delegate the remaining work to the wrapped object.
kotlinCopy code
interface Coffee {
fun cost(): Double
}
class SimpleCoffee : Coffee {
override fun cost() = 2.0
}
abstract class CoffeeDecorator(private val coffee: Coffee) : Coffee {
override fun cost() = coffee.cost()
}
class MilkDecorator(coffee: Coffee) : CoffeeDecorator(coffee) {
override fun cost() = super.cost() + 1.0
}
class SugarDecorator(coffee: Coffee) : CoffeeDecorator(coffee) {
override fun cost() = super.cost() + 0.5
}
fun main() {
val coffee: Coffee = SugarDecorator(MilkDecorator(SimpleCoffee()))
println("Cost: ${coffee.cost()}")
}

Using Delegation:

  1. Indirect Composition with Delegation: In this approach, decorators contain an instance of the component interface or class, and they delegate calls to it. This approach might feel more flexible since you’re not limited to the structure of a specific interface.
kotlinCopy code
interface Coffee {
fun cost(): Double
}
class SimpleCoffee : Coffee {
override fun cost() = 2.0
}
class MilkDecorator(private val coffee: Coffee) : Coffee {
override fun cost() = coffee.cost() + 1.0
}
class SugarDecorator(private val coffee: Coffee) : Coffee {
override fun cost() = coffee.cost() + 0.5
}
fun main() {
val baseCoffee: Coffee = SimpleCoffee()
val coffeeWithMilk: Coffee = MilkDecorator(baseCoffee)
val coffeeWithSugarAndMilk: Coffee = SugarDecorator(coffeeWithMilk)
println("Cost: ${coffeeWithSugarAndMilk.cost()}")
}

Differences:

Relationship Type:

  • Composition: Decorator class has a direct composition relationship with the component class or interface.
  • Delegation: Decorator class contains an instance of the component class or interface and delegates calls to it.

Flexibility:

  • Composition: More rigid structure due to the inheritance-based composition.
  • Delegation: More flexible since it can wrap any object that implements the component interface.

Inheritance vs. Composition:

  • Composition: Uses inheritance to achieve the decoration.
  • Delegation: Uses composition to achieve the decoration.

Extensibility:

  • Composition: Easier to extend with additional decorators.
  • Delegation: May require some extra management when dealing with multiple decorators.

Wrapper Responsibility:

  • Composition: Decorator classes handle both their own behavior and delegation to the wrapped component.
  • Delegation: Decorator classes only handle delegation to the wrapped component.

--

--