Design Principle
Decorator Pattern
Learn the Decorator pattern: attach responsibilities to objects dynamically. A flexible alternative to subclassing for extending functionality at runtime.
Decorator Pattern
Why This Matters
Think of the Decorator pattern like adding toppings to a pizza. You start with a base pizza, then add toppings (cheese, pepperoni, mushrooms) one at a time. Each topping adds something new without changing the base. The Decorator pattern does the same for code—it lets you add behavior to objects dynamically, one feature at a time.
This matters because sometimes you need to add features to objects, but subclassing creates a combinatorial explosion. If you have 3 base classes and 5 features, you'd need 3 × 2^5 = 96 subclasses. The Decorator pattern lets you compose features dynamically, avoiding this explosion.
In interviews, when someone asks "How would you add features to objects without subclassing?", they're testing whether you understand the Decorator pattern. Do you know how to compose decorators? Can you add behavior dynamically? Most engineers can't. They use inheritance and wonder why they have so many classes.
What Engineers Usually Get Wrong
Engineers often think "Decorator pattern is just composition." But Decorator pattern is more specific—it maintains the same interface as the component it decorates, and decorators can be composed. This allows you to stack decorators (e.g., add compression, then encryption, then logging) without the client knowing.
Engineers also don't understand when to use Decorator vs Strategy. Decorator adds behavior to objects (like adding features). Strategy chooses between algorithms (like choosing a sorting algorithm). Use Decorator when you need to add features dynamically. Use Strategy when you need to choose algorithms.
How This Breaks Systems in the Real World
A service was using inheritance to add features. They had classes like CompressedFile, EncryptedFile, CompressedEncryptedFile, LoggedFile, CompressedLoggedFile, etc. The class hierarchy exploded. Adding a new feature required creating many new classes. The fix? Use Decorator pattern. Create decorators for each feature (CompressionDecorator, EncryptionDecorator, LoggingDecorator). Compose them dynamically. Now adding a new feature is just creating one decorator, not many classes.
Another story: A service was using decorators but didn't maintain the same interface. Each decorator had a different interface, making composition impossible. The fix? Ensure decorators implement the same interface as the component they decorate. This allows decorators to be composed.
What is the Decorator Pattern?
Decorator Pattern provides:
- Dynamic behavior: Add behavior at runtime
- Composition over inheritance: Extend without subclassing
- Flexible combination: Combine multiple decorators
- Single responsibility: Each decorator adds one feature
Use cases:
- Adding features to objects dynamically
- Extending functionality without modifying classes
- Combining multiple features
- Stream processing (Java I/O streams)
Structure
Component (interface)
└─ operation()
ConcreteComponent (implements Component)
└─ operation()
Decorator (implements Component)
└─ component: Component
└─ operation() (calls component.operation())
ConcreteDecorator (extends Decorator)
└─ operation() (adds behavior, calls super.operation())
Examples
Basic Decorator Pattern
1// Component interface2interface Coffee {3 getCost(): number;4 getDescription(): string;5}67// Concrete component8class SimpleCoffee implements Coffee {9 getCost(): number {10 return 5;11 }1213 getDescription(): string {14 return "Simple coffee";15 }16}1718// Decorator19abstract class CoffeeDecorator implements
HTTP Request Decorator
1// Component2interface HTTPRequest {3 send(): Promise<Response>;4}56// Concrete component7class BasicHTTPRequest implements HTTPRequest {8 constructor(private url: string, private options: RequestInit) {}910 async send(): Promise<Response> {11 return await fetch(this.url, this.options);12 }13}1415// Decorator
Common Pitfalls
- Too many decorators: Can become complex. Fix: Keep decorators simple, limit nesting
- Order matters: Decorator order affects behavior. Fix: Document order, use builder pattern
- Performance overhead: Multiple layers add overhead. Fix: Consider performance impact
- Not using interfaces: Tight coupling. Fix: Use interfaces, dependency injection
Interview Questions
Beginner
Q: What is the Decorator pattern and how does it differ from inheritance?
A:
Decorator Pattern attaches additional responsibilities to objects dynamically.
Key characteristics:
- Dynamic behavior: Add behavior at runtime
- Composition: Uses composition instead of inheritance
- Flexible combination: Combine multiple decorators
- Single responsibility: Each decorator adds one feature
Example:
1let coffee: Coffee = new SimpleCoffee();2coffee = new MilkDecorator(coffee);3coffee = new SugarDecorator(coffee);4// Coffee now has milk and sugar
Difference from inheritance:
- Inheritance: Static, compile-time, creates new class
- Decorator: Dynamic, runtime, wraps existing object
Benefits:
- Flexibility: Add/remove features at runtime
- No class explosion: Don't need classes for every combination
- Single responsibility: Each decorator does one thing
Intermediate
Q: Explain how the Decorator pattern works. How do you compose multiple decorators?
A:
Decorator Pattern Structure:
1. Component Interface:
1interface Component {2 operation(): void;3}
2. Concrete Component:
1class ConcreteComponent implements Component {2 operation(): void {3 console.log("Base operation");4 }5}
3. Decorator (Abstract):
1abstract class Decorator implements Component {2 protected component: Component;34 constructor(component: Component) {5 this.component = component;6 }78 operation(): void {9 this.component.operation();10 }11}
4. Concrete Decorators:
1class ConcreteDecoratorA extends Decorator {2 operation(): void {3 super.operation();4 this.addedBehavior();5 }67 private addedBehavior(): void {8 console.log("Added behavior A");9 }10}
Composing Multiple Decorators:
1let component: Component = new ConcreteComponent();2component = new ConcreteDecoratorA(component);3component = new ConcreteDecoratorB(component);4component = new ConcreteDecoratorC(component);56component.operation();7// Output:8// Base operation9// Added behavior A10// Added behavior B11// Added behavior C
Order matters: Decorators are applied in reverse order of wrapping.
Senior
Q: Design a decorator system for a text processing pipeline that supports multiple transformations (encryption, compression, formatting) that can be applied in any order. Handle performance and ensure transformations are reversible.
A:
1// Component interface2interface TextProcessor {3 process(text: string): string;4 reverse(text: string): string; // For reversible transformations5}67// Concrete component8class PlainTextProcessor implements TextProcessor {9 process(text: string): string {10 return text;11 }1213 reverse(text: string): string {14 return text;15 }16}
Features:
- Reversible transformations: Each decorator can reverse
- Flexible composition: Add decorators in any order
- Performance: Can optimize based on decorator order
- Pipeline builder: Fluent interface for building pipelines
-
Decorator pattern: Adds behavior to objects dynamically
-
Composition over inheritance: Extends without subclassing
-
Flexible combination: Combine multiple decorators
-
Single responsibility: Each decorator adds one feature
-
Use cases: Adding features dynamically, stream processing, text processing
-
Best practices: Keep decorators simple, document order, consider performance
-
Adapter Pattern - Both patterns wrap objects. Decorator adds behavior, Adapter converts interface. Understanding both helps choose the right pattern.
-
Proxy Pattern - Both patterns wrap objects. Decorator adds behavior, Proxy controls access. Understanding both helps choose the right pattern.
-
Composite Pattern - Both patterns compose objects. Decorator composes features, Composite composes tree structures.
-
Composition Over Inheritance - Decorator pattern is a prime example of composition over inheritance. Understanding composition helps understand decorator benefits.
-
SOLID Principles - Decorator pattern follows Open/Closed Principle (extend without modify). Understanding SOLID helps understand decorator benefits.
Key Takeaways
Decorator pattern: Adds behavior to objects dynamically
Composition over inheritance: Extends without subclassing
Flexible combination: Combine multiple decorators
Single responsibility: Each decorator adds one feature
Use cases: Adding features dynamically, stream processing, text processing
Best practices: Keep decorators simple, document order, consider performance
Related Topics
Adapter Pattern
Both patterns wrap objects. Decorator adds behavior, Adapter converts interface. Understanding both helps choose the right pattern.
Proxy Pattern
Both patterns wrap objects. Decorator adds behavior, Proxy controls access. Understanding both helps choose the right pattern.
Composite Pattern
Both patterns compose objects. Decorator composes features, Composite composes tree structures.
Composition Over Inheritance
Decorator pattern is a prime example of composition over inheritance. Understanding composition helps understand decorator benefits.
SOLID Principles
Decorator pattern follows Open/Closed Principle (extend without modify). Understanding SOLID helps understand decorator benefits.
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.