Design Pattern (14) Decorator Pattern: Dynamic Feature Extension and Composition Design Guide

You can download the Design Pattern series code from this design_pattern repo.

After learning the Adapter Pattern, Bridge Pattern, and Composite Pattern, we have mastered several important concepts of structural patterns. Now let’s explore a pattern that can dynamically extend object functionality: the Decorator Pattern.

Requirements

We need to develop a flexible POS system for a boutique coffee shop. The challenge of this system lies in handling various combinations of coffee and add-ons.

Core Requirements:

  • Base Coffee Types: The system supports multiple base coffees (Espresso, House Blend, etc.)
  • Add-on Options: Each coffee can have multiple add-ons (milk, chocolate syrup, whipped cream, etc.)
  • Unlimited Combinations: Customers can combine different add-ons without restrictions

Technical Requirements:

  • Dynamic Composition: The system must support dynamically combining different add-ons at runtime
  • Price Calculation: Accurately calculate total prices for all combinations
  • Order Description: Provide clear order content descriptions

Design Challenges:

  • Combinatorial Explosion: If we create a class for every combination, the number of classes will grow exponentially
  • Extensibility: It should be easy to add new base coffees or add-ons in the future
  • Flexibility: Customers should be able to freely combine without restrictions

Object-Oriented Analysis (OOA)

Before diving into the design, let’s first conduct object-oriented analysis to identify the core elements in the coffee ordering system:

Identifying Forces

When dealing with dynamic composition requirements like the coffee ordering system, without using appropriate design patterns, we face the following serious challenges:

1. Combinatorial Explosion Crisis

Problem Scale:

  • 2 base coffees × 3 add-ons = at least 8 classes needed (no add-ons + single add-ons + double add-ons + triple add-ons)
  • If expanded to 5 base coffees and 5 add-ons, combinations can reach 2^5 × 5 = 160 types

Concrete Impact:

  • Number of classes grows exponentially, making the codebase difficult to manage
  • Each new add-on requires creating new classes for all existing combinations

2. Static Structure Limitations

Problem Description:

  • All possible combinations must be determined at compile time
  • Cannot dynamically add or remove add-ons at runtime
  • Cannot support special requirements like “double whipped cream” or “triple chocolate syrup”

Concrete Impact:

  • Customer personalization needs are difficult to satisfy
  • System’s business value is limited

3. High Coupling & Low Reusability

Problem Description:

  • Various combination classes lack common abstraction
  • Same add-on logic is repeatedly implemented in different combinations
  • When modifying an add-on’s price, updates are needed in multiple places

Concrete Impact:

  • High maintenance cost and error-prone
  • Slow new feature development

4. Poor Extensibility

Problem Description:

  • When adding new base coffee types, corresponding classes need to be created for each add-on combination
  • When adding new add-on types, corresponding classes need to be created for each existing combination
  • Any addition in later system development may become a massive project

Concrete Impact:

  • Slow product iteration, declining competitiveness
  • Development team productivity severely affected

Applying Decorator Pattern (Solution) to Achieve New Context (Resulting Context)

Facing the challenge of dynamic composition, the Decorator Pattern provides us with a powerful and elegant solution.

Core Concept of Decorator Pattern

The essence of the Decorator Pattern lies in “dynamically extending object functionality through wrapping”. Its core philosophy is:

  • Wrapper Inheritance: Decorators and decorated objects implement the same interface, making them interchangeable
  • Incremental Enhancement: Each decorator adds new functionality without modifying the original object
  • Recursive Composition: Multiple decorators can be nested and combined, forming chain structures

Real-Life Analogy

Imagine decorating a Christmas tree:

  1. Base Tree: This is our base coffee (Espresso)
  2. Add Lights: First layer of decoration (adding milk)
  3. Add Ribbons: Second layer of decoration (adding chocolate syrup)
  4. Add Ornaments: Third layer of decoration (adding whipped cream)

Each layer of decoration preserves the original beauty while adding new elements.

UML Structure of Decorator Pattern

Let’s first understand the standard structure of the Decorator Pattern:

Four Core Roles of Decorator Pattern:

1. Component (Component Interface)

  • Defines common interface for basic components and decorators
  • Ensures decorators and decorated objects can be interchangeable
  • In our example, this is the Beverage interface

2. ConcreteComponent (Concrete Component)

  • Concrete classes that implement basic functionality
  • This is the starting point of the decoration chain, providing the most basic functionality
  • In our example: Espresso and HouseBlend

3. Decorator (Decorator Base Class)

  • Maintains reference to Component, implements common logic for decoration behavior
  • Provides unified basic structure for all concrete decorators
  • In our example: CondimentDecorator

4. ConcreteDecorator (Concrete Decorator)

  • Implements specific decoration functionality, adding new behavior or state
  • Can add additional logic before or after calling the decorated object
  • In our example: Milk, ChocolateSyrup, WhippedCream

Applying to Our Coffee System

Now let’s apply the Decorator Pattern to the coffee ordering system:

Object-Oriented Programming (OOP)

Now let’s implement this Decorator Pattern design using Kotlin. We’ll gradually build each component of the coffee ordering system:

1. Component - Beverage Interface

First define the component interface, providing a unified operation interface for all coffee products:

interface Beverage {
    val description: String
    fun cost(): Double
}

2. ConcreteComponent - Base Coffee Types

Next implement concrete base coffee classes:

class Espresso : Beverage {
    override val description = "Espresso"
    override fun cost() = 1.99
}

class HouseBlend : Beverage {
    override val description = "House Blend Coffee"
    override fun cost() = 0.89
}

3. Decorator - CondimentDecorator Base Class

Define the abstract base class for decorators, providing unified structure for all add-ons:

abstract class CondimentDecorator(protected val beverage: Beverage) : Beverage() {
    override abstract val description: String
}

4. ConcreteDecorator - Specific Add-ons

Implement various concrete add-on decorators:

class Milk(beverage: Beverage) : CondimentDecorator(beverage) {
    override val description = "${beverage.description}, Milk"
    override fun cost() = beverage.cost() + 0.3
}

class ChocolateSyrup(beverage: Beverage) : CondimentDecorator(beverage) {
    override val description = "${beverage.description}, Chocolate Syrup"
    override fun cost() = beverage.cost() + 0.5
}

class WhippedCream(beverage: Beverage) : CondimentDecorator(beverage) {
    override val description = "${beverage.description}, Whipped Cream"
    override fun cost() = beverage.cost() + 0.4
}

Client Usage Example

Now let’s see how the Decorator Pattern allows customers to flexibly combine different coffees:

fun main() {
    // Make an Espresso
    val espresso = Espresso()
    println("${espresso.description}: $${espresso.cost()}")

    // Make an Espresso with Milk, Chocolate Syrup and Whipped Cream
    val customBeverage = WhippedCream(
        ChocolateSyrup(
            Milk(Espresso())
        )
    )
    println("${customBeverage.description}: $${customBeverage.cost()}")

    // Make an HouseBlend with Milk and double Whipped Cream
    val layeredBeverage = WhippedCream(
        WhippedCream(
            Milk(HouseBlend())
        )
    )
    println("${layeredBeverage.description}: $${layeredBeverage.cost()}")
}

[Output]

Espresso: $1.99
Espresso, Milk, Chocolate Syrup, Whipped Cream: $3.19
House Blend, Milk, Whipped Cream, Whipped Cream: $2.49

Execution Results and Analysis

When we execute the above code, we get the following output:

Espresso: $1.99
Espresso, Milk, Chocolate Syrup, Whipped Cream: $3.19
House Blend, Milk, Whipped Cream, Whipped Cream: $2.49

This result perfectly demonstrates the power of the Decorator Pattern:

  • First line: Pure base Espresso
  • Second line: Rich Espresso with multiple layers of decoration
  • Third line: Even the same add-ons can be added repeatedly

Conclusion

By applying the Decorator Pattern, we successfully solved all challenges of dynamic composition:

Core Benefits Achieved:

1. Incremental Feature Extension

  • Each decorator focuses only on its specific functionality with single, clear responsibility
  • Adds new functionality through wrapping without modifying the original object
  • Complies with the Open-Closed Principle

2. Complete Solution to Combinatorial Explosion

  • Number of classes reduced from O(m^n) to O(m+n)
  • 2 base coffees + 3 add-ons = only 5 classes needed
  • Adding add-ons or base coffees only requires adding one class each

3. Unlimited Flexibility

  • Supports arbitrary order and number of combinations
  • Can dynamically combine different add-ons at runtime
  • Supports nested combinations, meeting personalization needs

4. Elegant Code Structure

  • High decoupling between decorators, can be developed and tested independently
  • Same code structure makes it easy for newcomers to get started
  • High readability and maintainability

Practical Application Scenarios:

Decorator Pattern is particularly useful in:

  • Beverage Ordering Systems: Like Starbucks, Noble Family add-on configurations
  • GUI Components: Adding borders, scrollbars, shadows to buttons, text boxes
  • IO Stream Processing: Like Java’s BufferedReader, FileReader hierarchical wrapping
  • Middleware: Adding logging, authentication, caching functionality to web requests

Relationships with Other Patterns:

Decorator Pattern complements patterns we’ve learned before:

  • With Composite Pattern: Both use recursive structures, but with different purposes (decoration vs structural organization)
  • With Adapter Pattern: Both change object behavior, but in different ways (adaptation vs enhancement)
  • With Bridge Pattern: Both focus on flexible design, but solve different problems

Through the Decorator Pattern, we learned how to elegantly handle dynamic feature extension. This design thinking provides a solid foundation for learning more complex design patterns later.

Series Navigation

Structural Design Pattern Series

  • Adapter Pattern - Making incompatible interfaces work together
  • Bridge Pattern - Separating abstraction from implementation, supporting independent evolution
  • Composite Pattern - Uniformly handling individual objects and object combinations
  • Facade Pattern - Providing unified interface to simplify complex subsystems
  • Flyweight Pattern - Efficiently managing memory usage of large numbers of similar objects
  • Proxy Pattern - Controlling resource access through smart proxy objects

Behavioral Design Pattern Series

Creational Design Pattern Basics

Through the Decorator Pattern, we mastered core techniques for dynamic feature extension. In the next article on the Facade Pattern, we will explore how to simplify access to complex subsystems through unified interfaces.




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • Claude Code 使用技巧與最佳實踐 - Tips and Best Practices
  • 🤖 AI Agent Series (Part 1): Understanding the Core Interaction Logic of LLM, RAG, and MCP
  • 💡 Managing Multiple GitHub Accounts on One Computer: The Simplest SSH Configuration Method
  • 🚀 How to Use Excalidraw AI to Quickly Generate Professional Diagrams and Boost Work Efficiency!
  • Complete macOS Development Environment Setup Guide: Mobile Development Toolchain Configuration Tutorial