Decorator Pattern
The Decorator Pattern allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. It provides a flexible alternative to subclassing for extending functionality.
This pattern is particularly useful when you want to add responsibilities to objects without modifying their underlying code, following the Open/Closed Principle.
Problem
Imagine you're building a coffee ordering system. You have a basic coffee, but customers can customize it with various add-ons like milk, sugar, whipped cream, or chocolate. Creating a separate class for each combination would lead to an explosion of classes. How can you add these features dynamically without creating a huge class hierarchy?
Components
- Component: Defines the interface for objects that can have responsibilities added to them dynamically.
- ConcreteComponent: Defines an object to which additional responsibilities can be attached.
- Decorator: Maintains a reference to a Component object and defines an interface that conforms to Component's interface.
- ConcreteDecorator: Adds responsibilities to the component.
Implementation Example
Let's implement a coffee ordering system using the Decorator pattern:
// Component Interface
class Coffee {
getCost() {
// To be implemented by concrete components
}
getDescription() {
// To be implemented by concrete components
}
}
// Concrete Component
class SimpleCoffee extends Coffee {
getCost() {
return 5;
}
getDescription() {
return "Simple Coffee";
}
}
// Base Decorator
class CoffeeDecorator extends Coffee {
constructor(decoratedCoffee) {
super();
this.decoratedCoffee = decoratedCoffee;
}
getCost() {
return this.decoratedCoffee.getCost();
}
getDescription() {
return this.decoratedCoffee.getDescription();
}
}
// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
constructor(decoratedCoffee) {
super(decoratedCoffee);
}
getCost() {
return this.decoratedCoffee.getCost() + 1;
}
getDescription() {
return this.decoratedCoffee.getDescription() + ", with milk";
}
}
class WhipDecorator extends CoffeeDecorator {
constructor(decoratedCoffee) {
super(decoratedCoffee);
}
getCost() {
return this.decoratedCoffee.getCost() + 2;
}
getDescription() {
return this.decoratedCoffee.getDescription() + ", with whipped cream";
}
}
class ChocolateDecorator extends CoffeeDecorator {
constructor(decoratedCoffee) {
super(decoratedCoffee);
}
getCost() {
return this.decoratedCoffee.getCost() + 1.5;
}
getDescription() {
return this.decoratedCoffee.getDescription() + ", with chocolate";
}
}
// Usage
let myCoffee = new SimpleCoffee();
console.log(myCoffee.getDescription() + ": $" + myCoffee.getCost());
Interactive Demo: Coffee Customizer
Build your own coffee by adding decorators! Select different options to see how the Decorator pattern works in action.
When to Use
- When you need to add responsibilities to objects dynamically and transparently, without affecting other objects
- When extending functionality by subclassing is impractical (would lead to an explosion of subclasses)
- When you want to add functionality to individual objects without affecting others of the same class
- When you need to combine multiple behaviors in various ways
Benefits
- More flexibility than static inheritance - you can add or remove responsibilities at runtime
- Avoids feature-laden classes high up in the hierarchy
- Follows the Single Responsibility Principle by separating concerns into different classes
- Follows the Open/Closed Principle by allowing behavior extension without modifying existing code
Real-World Uses
- Java I/O Stream classes (BufferedReader, LineNumberReader, etc.)
- UI component libraries where components can be enhanced with borders, scrolling, etc.
- Web frameworks with middleware patterns (like Express.js middleware)
- Notification systems with different delivery methods (email, SMS, etc.)
- Logging frameworks with various output formats and destinations