Unlocking the Power of the Decorator Pattern

A Dynamic Approach to Extending Object Functionality in App development.

Shubham
8 min readJun 14, 2023

As I mentioned in my previous article on the Strategy design pattern, where I took a deep dive into its concepts and implementations, I promised to explore another powerful design pattern in depth: the Decorator pattern. In this article, we will embark on a pragmatic journey into the Decorator pattern, unraveling its principles, practical implementation approaches, and real-world use cases.

I must warn you that this is going to be a long read, but I assure you that by sticking with me, you will gain a thorough understanding of all the ins and outs of the Decorator pattern. Breaking this article into multiple parts would disrupt the flow and hinder our ability to explore this design pattern comprehensively.

So, let’s dive into the world of the Decorator pattern together, embarking on a pragmatic journey that will unravel its inner workings and reveal its flexibility and dynamic nature. By the end of this article, you will have a solid grasp of how to leverage the Decorator pattern to enhance and scale up the functionality of your software designs.

Introduction:

The Decorator design pattern is a structural pattern that allows adding new functionality to an existing object dynamically. It provides an alternative to subclassing for extending the functionality of an object at runtime. This pattern follows the principle of open-closed design, where classes are open for extension but closed for modification. In this article, we will explore the Decorator pattern, its common implementations in Swift and Kotlin, alternatives to consider, and situations where it is best to avoid using this pattern.

Definition of the Decorator Pattern:

The Decorator pattern enhances the behaviour of an object by dynamically wrapping it with one or more decorators. These decorators are responsible for adding or modifying the functionality of the wrapped object without changing its interface. By stacking multiple decorators, you can incrementally extend the behavior of the original object.

Let’s explore common implementations of the Decorator pattern using Swift and Kotlin.

Swift Implementation:

In Swift, you can use protocols and extensions to implement the Decorator pattern effectively. Here’s an example:

protocol Coffee {
func getCost() -> Double
func getDescription() -> String
}

class SimpleCoffee: Coffee {
func getCost() -> Double {
return 1.0
}

func getDescription() -> String {
return "Simple coffee"
}
}

protocol CoffeeDecorator: Coffee {
var decoratedCoffee: Coffee { get }
}

extension CoffeeDecorator {
func getCost() -> Double {
return decoratedCoffee.getCost()
}

func getDescription() -> String {
return decoratedCoffee.getDescription()
}
}

class MilkDecorator: CoffeeDecorator {
let decoratedCoffee: Coffee

init(_ coffee: Coffee) {
self.decoratedCoffee = coffee
}

func getCost() -> Double {
return decoratedCoffee.getCost() + 0.5
}

func getDescription() -> String {
return decoratedCoffee.getDescription() + ", with milk"
}
}

// Usage
let simpleCoffee: Coffee = SimpleCoffee()
let coffeeWithMilk: Coffee = MilkDecorator(simpleCoffee)

print(coffeeWithMilk.getDescription()) // Output: Simple coffee, with milk
print(coffeeWithMilk.getCost()) // Output: 1.5

Kotlin Implementation:

In Kotlin, you can use interfaces and classes to implement the Decorator pattern. Here’s an example:

interface Coffee {
fun getCost(): Double
fun getDescription(): String
}

class SimpleCoffee : Coffee {
override fun getCost(): Double {
return 1.0
}

override fun getDescription(): String {
return "Simple coffee"
}
}

abstract class CoffeeDecorator(private val decoratedCoffee: Coffee) : Coffee {
override fun getCost(): Double {
return decoratedCoffee.getCost()
}

override fun getDescription(): String {
return decoratedCoffee.getDescription()
}
}

class MilkDecorator(decoratedCoffee: Coffee) : CoffeeDecorator(decoratedCoffee) {
override fun getCost(): Double {
return super.getCost() + 0.5
}

override fun getDescription(): String {
return super.getDescription() + ", with milk"
}
}

// Usage
val simpleCoffee: Coffee = SimpleCoffee()
val coffeeWithMilk: Coffee = MilkDecorator(simpleCoffee)

println(coffeeWithMilk.getDescription()) // Output: Simple coffee, with milk
println(coffeeWithMilk.getCost()) // Output: 1.5

The code snippet showcases the usage of the Decorator pattern to enhance the functionality of a coffee ordering system.

The “CoffeeDecorator” class serves as the base decorator, inheriting from the “Coffee” class, which represents a basic coffee order. The “CoffeeDecorator” class has an instance variable to hold a reference to the decorated coffee object and overrides the cost calculation method to include any additional costs or modifications.

In the given code, we have a specific decorator called “MilkDecorator” that extends the functionality of the base coffee order by adding milk. The “MilkDecorator” class inherits from “CoffeeDecorator” and overrides the cost calculation method to include the cost of milk in addition to the base coffee cost.

By using decorators, we can dynamically wrap the coffee object with one or more decorators to add new functionalities or modifications. This approach allows us to scale up the functionality of the coffee ordering system without modifying the core “Coffee” class.

For example, we can create additional decorators such as “WhippedCreamDecorator,” “CaramelDecorator,” or “VanillaDecorator” to introduce whipped cream, caramel flavor, or vanilla flavor to the coffee, respectively. These decorators can be combined in various ways to achieve different customizations.

Other Common Use Cases:

The Decorator pattern finds application in various scenarios in app development. Some common use cases where the Decorator pattern is employed include:

  1. User Interface Customization: Decorators can be utilized to dynamically customize the appearance and behavior of user interface elements. For example, in a mobile app, you can apply decorators to buttons, labels, or views to add additional functionalities such as animations, special effects, or dynamic styling.
  2. Input/Output Stream Modification: Decorators are often used to extend the functionality of input/output streams. By wrapping streams with decorators, you can introduce additional features like encryption, compression, or logging without modifying the existing stream classes. This approach enables a flexible and modular way to modify data as it is being read from or written to a stream.
  3. Logging and Instrumentation: Decorators can be employed for logging and instrumentation purposes. By applying decorators to methods or classes, you can track method invocations, measure performance, log debug information, or add other monitoring capabilities. This allows you to gather valuable runtime data without altering the original implementation.
  4. Caching: The Decorator pattern is useful for implementing caching mechanisms. By introducing decorators, you can transparently add caching functionality to methods or data retrieval processes. This can significantly improve performance by reducing expensive computations or database queries, especially for frequently accessed or computationally intensive operations.
  5. Authorization and Authentication: Decorators can be employed to add authorization and authentication checks to methods or components. By wrapping objects with decorators, you can enforce security measures, such as verifying user permissions or validating authentication tokens, before executing the underlying logic. This provides a modular approach to integrate security features into various parts of the application.
  6. Dynamic Feature Addition: The Decorator pattern enables the dynamic addition of features to an application. By using decorators, you can selectively enable or disable specific functionalities based on user preferences, subscription plans, or other runtime conditions. This allows for customizable and extensible applications without cluttering the core codebase.

These are just a few examples of how the Decorator pattern can be applied in app development. Its flexibility and ability to add functionalities dynamically make it a valuable tool for designing modular and extensible software systems.

Let’s take a look at how we can use decorator pattern to customize UI elements :

Swift Implementation:

import UIKit

// Base UI component interface
protocol UIComponent {
func draw()
}

// Concrete UI component
class Button: UIComponent {
func draw() {
print("Drawing a button")
}
}

// Decorator
class Decorator: UIComponent {
private let component: UIComponent

init(component: UIComponent) {
self.component = component
}

func draw() {
component.draw()
}
}

// Concrete decorator for adding a border
class BorderDecorator: Decorator {
override func draw() {
super.draw()
addBorder()
}

private func addBorder() {
print("Adding border to the component")
}
}

// Concrete decorator for applying a background color
class BackgroundColorDecorator: Decorator {
private let color: UIColor

init(component: UIComponent, color: UIColor) {
self.color = color
super.init(component: component)
}

override func draw() {
super.draw()
applyBackgroundColor()
}

private func applyBackgroundColor() {
print("Applying background color: \(color)")
}
}

// Usage
let button: UIComponent = Button()
let buttonWithBorder: UIComponent = BorderDecorator(component: button)
let buttonWithBorderAndColor: UIComponent = BackgroundColorDecorator(component: buttonWithBorder, color: .red)

buttonWithBorderAndColor.draw()

Kotlin:

interface UIComponent {
fun draw()
}

class Button : UIComponent {
override fun draw() {
println("Drawing a button")
}
}

class Decorator(private val component: UIComponent) : UIComponent {
override fun draw() {
component.draw()
}
}

class BorderDecorator(component: UIComponent) : Decorator(component) {
override fun draw() {
super.draw()
addBorder()
}

private fun addBorder() {
println("Adding border to the component")
}
}

class BackgroundColorDecorator(private val component: UIComponent, private val color: String) : UIComponent {
override fun draw() {
component.draw()
applyBackgroundColor()
}

private fun applyBackgroundColor() {
println("Applying background color: $color")
}
}

fun main() {
val button: UIComponent = Button()
val buttonWithBorder: UIComponent = BorderDecorator(button)
val buttonWithBorderAndColor: UIComponent = BackgroundColorDecorator(buttonWithBorder, "red")

buttonWithBorderAndColor.draw()
}

In both examples, we have a base UI component (Button) and decorators (BorderDecorator and BackgroundColorDecorator) that add specific customization to the component. The decorators wrap the base component and enhance its behavior by adding borders and applying background colors. The result is a customizable UI component that can be easily extended with additional decorators for further customization.

When to Avoid Using the Decorator Pattern:

As we explore various techniques and patterns, it’s natural to feel a strong inclination to use them regardless of the actual need. However, it is essential to exercise caution and understand when it is appropriate to employ a particular pattern and when it may be better to avoid it.

The Decorator pattern may not be suitable in the following scenarios:

  1. When there are many combinations of extensions: If the number of possible combinations of extensions is large, the Decorator pattern can lead to an explosion of classes. In such cases, other patterns or approaches should be considered.
  2. Performance-sensitive applications: The Decorator pattern introduces additional layers of abstraction, which can impact performance in highly performance-sensitive applications. In these cases, a more optimized solution might be preferred.

Alternatives to the Decorator Pattern:

While the Decorator pattern offers a flexible way to extend object behavior, there are alternative approaches to consider:

  1. Inheritance: In some cases, extending functionality through inheritance might be a simpler and more straightforward solution, especially if the extension is fixed and known in advance.
  2. Composition: Instead of wrapping objects with decorators, you can achieve similar results using composition. By creating a new class that holds a reference to the original object and provides additional functionality, you can extend the behavior without modifying the original object’s class.

In conclusion, the Decorator pattern offers a dynamic and flexible approach to extend object functionality at runtime, without modifying the underlying structure. By leveraging composition and decorators, we can incrementally add or modify features, ensuring code reusability and maintainability. Practical implementations in Swift and Kotlin showcased its versatility in user interface customization, stream modification, logging, caching, and more. Understanding alternatives and evaluating suitability for each use case is vital. Mastering the Decorator pattern empowers scalable and adaptable software designs. Embrace its potential to create flexible and maintainable solutions, unlocking the power of the Decorator pattern in your applications.

If you found this content valuable, I invite you to follow me for more insights, tips, and updates. Stay connected and be among the first to access new content. Let’s embark on this knowledge journey together. Click the follow button and stay informed!

--

--

Shubham

I speak for the new world. Seasoned Software Engineering Expert | Guiding Developers Towards Excellence | 12 Years of Industry Insight