Structural Patterns
Decorator
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Decorator Pattern — Wrapping Layers
The Decorator Pattern (also known as Wrapper) is a structural design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. By utilizing composition instead of inheritance, it bypasses the classic subclass explosion problem.
1. Learning Objectives
- Identify the limitations of inheritance for compounding cross-cutting concerns.
- Understand the structural layout of the Decorator pattern: Components, Concrete Components, Decorators, and Concrete Decorators.
- Analyze JVM memory layouts, nested stack delegations, and heap references of layered wrappers.
- Compare structural mechanics of Decorator with Proxy and Adapter patterns.
- Implement thread-safe, typed decorators in Java, Python, and C++ (using smart pointer aggregation).
2. Problem & Naive Solution
Suppose you are building a POS system for a coffee shop. You start with a base Beverage class. Customers can customize beverages by adding toppings: Milk, Sugar, Whip Cream, Soy, or Caramel.
The Naive Solution (Subclass Explosion)
A naive implementation uses inheritance to model every permutation of beverage and toppings:
SimpleCoffeeWithMilkSimpleCoffeeWithMilkAndSugarSimpleCoffeeWithMilkSugarAndWhipEspressoWithSoyAndCaramel
If you have 5 beverages and 5 toppings, you will end up with dozens of class files. This is known as Class Explosion.
Alternatively, you might add boolean flag fields to the base class:
This approach violates the Open/Closed Principle:
- Adding a new ingredient (e.g. Soy) forces you to modify the base
Beverageclass. - Double servings of an ingredient (e.g. double sugar) cannot be easily tracked using simple booleans.
- If the price of milk changes, you must rewrite the main cost calculation logic.
3. Issues
Relying on inheritance binds the subclass extension statically at compile-time. If a customer wants to dynamically add toppings at runtime or swap ingredients, inheritance fails completely because an object's class type cannot be changed after creation.
4. Pattern Introduction & UML
The Decorator Pattern shifts the architectural model from static subclassing to dynamic object aggregation. It defines a Wrapper class that implements the same interface as the wrapped object, delegating methods to the wrapped instance while adding custom behavior before or after execution.
UML: Decorator Structure
5. Participants
- Component (
Beverage): The common interface defining the operations that can be decorated dynamically. - Concrete Component (
SimpleCoffee): The core object whose behavior will be extended. - Decorator (
BeverageDecorator): An abstract class holding a reference to a Component and conforming to the Component's interface contract. - Concrete Decorator (
MilkDecorator,SugarDecorator): Dynamic subclasses that add specific properties and forward calls to the parent delegate.
6. Theory (Dynamic Composition vs Inheritance)
Decorator leverages composition to attach behavior recursively:
- Dynamic Behavior: Because a decorator implements the same interface as the wrapped object, a decorator can wrap another decorator. This allows you to construct deep nesting (e.g.
Whip(Milk(Sugar(Coffee)))). - Comparison: - Decorator: Dynamically adds/enriches the interface's behavior at runtime. - Proxy: Manages lifecycle, security, or access to the underlying object. - Adapter: Modifies the interface format itself to bridge structural gaps.
7. Syntax Explanation
Key implementation constructs in different languages:
- Java: Declares an abstract class extending the interface, keeping a reference to the delegate as a field (
protected final Beverage decoratedBeverage;). - Python: Interfaces are implicit (Duck Typing). A decorator class intercepts methods via
__getattr__or overrides standard properties to invoke delegates. - C++: Uses
std::unique_ptrcomposition to manage wrapping ownership, preventing resource leaks when nesting deep heap allocations.
8. Step-by-Step Implementation
- Step 1: Create the component interface outlining the shared contract methods (e.g.
Beverage). - Step 2: Implement concrete component classes representing base types (e.g.
SimpleCoffee). - Step 3: Create the abstract
BeverageDecoratorimplementingBeverage, injecting aBeveragecomponent reference via the constructor. - Step 4: Build concrete decorators subclassing the abstract decorator, adding value to the methods by delegating, modifying arguments, or modifying return values.
- Step 5: Instantiate the components by nesting them dynamically (e.g.
new MilkDecorator(new SugarDecorator(new SimpleCoffee()))).
9. Complete Code (Mini Project)
10. Code Walkthrough
Let's trace how the dynamic cost and description resolution works:
- Recursive Call Execution: When the client calls
getCost()on the outermost decorator (WhipCreamDecorator), it delegates execution by callingdecoratedBeverage.getCost(). This chains down toMilkDecorator, then toSugarDecorator, and finally returns2.0fromSimpleCoffee. - Bubbling Up: Each wrapping layer intercepts the return value of the delegate and appends its cost delta (e.g. adding
+0.50for milk,+0.20for sugar) before passing it up the call stack. - Encapsulated Type Consistency: Because both decorators and components implement the common
Beverageinterface, they are polymorphic. The client is completely unaware that it is interacting with a wrapped chain rather than a simple class object.
11. Execution Flow
- Client Invocation: Client invokes
getCost()on the wrapper instance. - Downwards Delegation: Each decorator forwards the call to its internal
Beveragereference. - Core Execution: The concrete component (
SimpleCoffee) calculates the base cost (2.00) and returns it. - Upwards Accumulation: As the call stack unwinds, each layer adds its ingredient surcharge.
- Client Delivery: The aggregated total ($2.70) is returned to the client.
12. Internal Working (JVM & Heap Analysis)
Using deep decorator hierarchies affects runtime performance:
- Recursive Stack Allocation: Nesting $N$ decorators creates $N$ nested method calls. Each call pushes a new stack frame onto the thread's execution stack, introducing stack frame lookup overhead.
- Heap Reference Graphs: The decorators form a linked-list hierarchy on the heap. Instead of a single flat object allocation, memory is occupied by multiple small wrapper objects pointing to each other. This layout increases pointer-chasing cost during execution and creates GC work when dereferencing the wrapper chain.
13. Complexity Analysis
- Time Complexity: $O(N)$ where $N$ is the number of decorators in the wrapper chain, due to recursive delegation.
- Space Complexity: $O(N)$ runtime stack space and heap allocation space to manage the linked components.
14. Best Practices
- Keep Interfaces Small: The component interface should focus strictly on fundamental behaviors. Decorating a large interface with 50 methods is difficult because every decorator must implement all 50 delegate methods.
- Ensure Ordering Independence: Avoid building decorators that must run in a specific order (e.g.
TaxDecoratormust run *after*DiscountDecorator). Decorators should ideally be composable in any sequence.
15. Common Mistakes
- Type Casting inside Clients: Checking if a component is an instance of a concrete decorator (e.g.,
if (beverage instanceof MilkDecorator)). This breaks runtime polymorphism and client-side decoupling. - Too Many Small Classes: Generating decorator classes for micro-behaviors that could easily be parameter fields in existing wrappers.
16. Framework Usage
- Java I/O Streams: Standard libraries use decorator heavily:
InputStreamReader adapts the raw InputStream socket to a character stream; BufferedReader decorates it with buffer caching.
17. Interview Discussion
Answer: Subclassing extends behavior statically at compile time, leading to class explosion for multi-axis configurations. Decorator extends behavior dynamically at runtime through composition, keeping the class count minimal at the expense of creating many small heap-allocated wrapper objects and deep recursive call stacks.
Answer: If decorators require access to private state details, the pattern's clean encapsulation is compromised. This is a design smell indicating the Component interface is either too narrow, or that a Mediator/Strategy pattern would be a better fit.
Answer: It is not supported by standard interfaces because the wrapping is uni-directional. To remove a decorator, you must reconstruct the object chain from the bottom up, skipping the target wrapper, or maintain an array representation of the decorators.
18. Practice Exercises
- Easy: Implement a
SugarDecoratorandWhipDecoratorin Python, verifying cost sums and description outputs. - Medium: Design a
TextFormattersystem withBoldDecorator,ItalicDecorator, andUnderlineDecoratorwrapping base text strings to output formatted HTML fragments. - Hard: Build an encryption/compression data stream wrapper. The base system writes raw bytes to a file. Design decorators that compress bytes (using GZip) and encrypt bytes (using AES) on-the-fly during streams.
19. Challenge Problem
Design a Dynamic Pricing Engine for a ridesharing application. The base cost of a ride depends on distance and time. However, rides can accrue multiple dynamic surcharges: Surge Pricing (multiplier), Night Surcharge (flat fee), Toll Road Surcharges (flat fee), and Promotional Discounts (percentage reduction). Design a decorator-based calculator. Surcharges must be stacked dynamically in any configuration. Surcharges that multiply cost must be calculated correctly with respect to flat fees. Write the implementation in Java, Python, or C++ and test it with multiple combinations of surcharges.
20. Summary & Cheat Sheet
- Decorator delegates execution to a wrapped component, attaching behavior before or after execution.
- Avoid inheritance explosion by stacking lightweight wrappers.
- Keep component interfaces small to simplify writing delegates.
- Do not perform type casting (
instanceof) on decorated instances.
21. Quiz
1. Which structural design pattern attaches responsibilities to objects dynamically at runtime?
A) Adapter
B) Proxy
C) Decorator (Correct)
2. What architectural problem does the Decorator pattern primarily solve?
A) Tight database query coupling
B) Class/subclass explosion (Correct)
C) Socket connection timeout management
3. How does the Decorator link to the Component it wraps?
A) Via global database tables
B) Via object composition by referencing the Component interface (Correct)
C) Via static factory configuration maps
4. Why is a Decorator class required to implement the same interface as the wrapped Component?
A) To support polymorphic substitution and wrapper nesting (Correct)
B) To access private class variables of the Component
C) To enable garbage collection of the core object
5. Which of the following is a classic example of the Decorator pattern in the Java Standard Library?
A) BufferedReader wrapping InputStreamReader (Correct)
B) ArrayList wrapping Arrays
C) Math.max()
6. What is the time complexity of resolving a call in a chain of $N$ decorators?
A) $O(1)$
B) $O(N)$ (Correct)
C) $O(\log N)$
7. What is a key performance drawback of using deeply nested decorators?
A) Disables multithreading execution safety
B) Creates recursive stack frame allocation and heap pointer chasing overhead (Correct)
C) Increases static class compilation size
8. How does Decorator differ from Adapter?
A) Decorator extends functionality under the same interface; Adapter matches incompatible interfaces (Correct)
B) Decorator modifies compile-time bytecodes; Adapter does not
C) Decorator requires multiple class inheritance; Adapter uses composition
9. Why should decorator component interfaces be kept small?
A) To prevent stack-overflow limits
B) To simplify implementation, as decorators must override and delegate all interface methods (Correct)
C) To avoid double-checked locking hazards
10. Can a decorator chain be altered easily by removing a middle wrapper?
A) Yes, because Java references are bi-directional
B) No, since wrapper pointers are uni-directional, rebuilding the chain is usually necessary (Correct)
C) Only when using abstract class decorators
22. Next Lesson Preview
In the next lesson, we will explore the Composite Pattern. We will learn how to treat individual objects and compositions of objects uniformly, enabling recursive tree structure hierarchies!
Related Topics
- AdapterConvert the interface of a class into another interface clients expect, allowing incompatible classes to work together
- FacadeProvide a unified, simplified interface to a set of interfaces in a subsystem, making the subsystem easier to use and decoupling clients.
- CompositeCompose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.