The Decorator Design Pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. This pattern is particularly useful for adhering to the Open/Closed Principle, one of the core principles of object-oriented design, which states that software entities should be open for extension, but closed for modification.
Concept of the Decorator Pattern
The primary objective of the Decorator pattern is to attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. In this pattern, a class will add functionality to another class, without changing the other classes' structure.
Key Components of the Decorator Pattern
Component: This is an interface for objects that can have responsibilities added to them dynamically.
Concrete Component: 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.
Concrete Decorators: Decorators that add responsibilities to the component.
When to Use the Decorator Pattern
Enhancing Functionality: When you need to add responsibilities to individual objects dynamically and transparently, without affecting other objects.
Reducing Sub classing: When extending functionality by sub classing is impractical because it leads to a large hierarchy of classes.
Configurable Additions: When you need to add functionalities that can be chosen at runtime.
Real-World Example: A Coffee-Making Application
Imagine a coffee shop app where customers can customize their coffee. Customers can start with a basic coffee and then add customizations like milk, sugar, or whipped cream. Using the Decorator pattern, you can start with a simple coffee and dynamically add features to it.
Practical Implementation: Customizable Coffee
// Component
class Coffee {
cost() {}
}
// Concrete Component
class SimpleCoffee extends Coffee {
cost() {
return 10; // base cost of coffee
}
}
// Decorator
class CoffeeDecorator extends Coffee {
constructor(coffee) {
super();
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
}
// Concrete Decorators
class WithMilk extends CoffeeDecorator {
cost() {
return super.cost() + 2; // cost of coffee plus milk
}
}
class WithSugar extends CoffeeDecorator {
cost() {
return super.cost() + 1; // cost of coffee plus sugar
}
}
class WithWhippedCream extends CoffeeDecorator {
cost() {
return super.cost() + 5; // cost of coffee plus whipped cream
}
}
// Client code
let myCoffee = new SimpleCoffee();
console.log(`Cost of Coffee: $${myCoffee.cost()}`);
myCoffee = new WithMilk(myCoffee);
console.log(`Cost of Coffee with milk: $${myCoffee.cost()}`);
myCoffee = new WithSugar(myCoffee);
console.log(`Cost of Coffee with sugar: $${myCoffee.cost()}`);
myCoffee = new WithWhippedCream(myCoffee);
console.log(`Cost of Coffee with whipped cream: $${myCoffee.cost()}`);
Conclusion
The Decorator Pattern offers an elegant solution for enhancing the functionality of objects dynamically while keeping the code flexible and maintainable. It is highly useful in scenarios where the capabilities of objects need to be augmented or adjusted on the fly according to user preferences or system requirements. This pattern is widely used in designing systems where new optional features need to be added without modifying existing codebases, such as graphical user interfaces, streaming media services, and others. By providing a way to extend functionality while adhering to the Open/Closed Principle, the Decorator Pattern is a valuable tool in a developer’s toolkit.