The Decorator Pattern is a structural design pattern that allows you to dynamically add or modify behavior of an object at runtime, without altering its structure. It provides an alternative to subclassing, where functionality is added to an object without changing its core class.
The core idea behind the Decorator Pattern is to “wrap” an object with a new behavior (or functionality) while preserving the interface of the original object.
Intent
- Add responsibilities to an object dynamically.
- The decorator class wraps the original class to extend its behavior.
- It’s used when you need to add functionality to individual objects, rather than to entire classes.
Structure of the Decorator Pattern
The pattern typically involves the following participants:
- Component:
This is the base interface or abstract class that defines the core functionality. - ConcreteComponent:
This is the class that implements theComponent
interface and represents the core object being decorated. It contains the default behavior. - Decorator (abstract class):
This abstract class implements theComponent
interface and contains a reference to aComponent
object. The decorator delegates the core functionality to the wrapped object while extending or modifying behavior. - ConcreteDecorator:
This extends theDecorator
class and adds new functionality by overriding methods from theComponent
interface.
UML Diagram of Decorator Pattern
+-------------------+
| Component | <-----------------------------+
+-------------------+ |
| +operation() | |
+-------------------+ |
^ |
| |
+-------------------+ +-------------------+ |
| ConcreteComponent | | Decorator | |
+-------------------+ +-------------------+ |
| +operation() | | -component: Component | |
+-------------------+ +-------------------+ |
^ | +operation() | |
| +-------------------+ |
+-------------------+ ^ |
| ConcreteDecorator | | |
+-------------------+ | |
| +operation() | | |
+-------------------+ | |
Example in Java
Here’s a simple example of the Decorator Pattern in Java:
Component Interface
interface Coffee {
double cost();
}
ConcreteComponent
class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 5; // Base cost for a simple coffee
}
}
Decorator Class
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public double cost() {
return decoratedCoffee.cost();
}
}
ConcreteDecorator
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return decoratedCoffee.cost() + 1.5; // Adds cost of milk
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return decoratedCoffee.cost() + 0.5; // Adds cost of sugar
}
}
Client Code
public class DecoratorPatternDemo {
public static void main(String[] args) {
Coffee simpleCoffee = new SimpleCoffee();
System.out.println("Cost of Simple Coffee: $" + simpleCoffee.cost());
// Adding milk to the coffee
Coffee milkCoffee = new MilkDecorator(simpleCoffee);
System.out.println("Cost of Coffee with Milk: $" + milkCoffee.cost());
// Adding sugar to the coffee with milk
Coffee milkSugarCoffee = new SugarDecorator(milkCoffee);
System.out.println("Cost of Coffee with Milk and Sugar: $" + milkSugarCoffee.cost());
}
}
Output:
Cost of Simple Coffee: $5.0
Cost of Coffee with Milk: $6.5
Cost of Coffee with Milk and Sugar: $7.0
Key Points of the Decorator Pattern:
- Flexibility:
The Decorator Pattern allows you to mix and match different functionalities in a flexible and dynamic way. You can add or remove behaviors at runtime. - Composition over Inheritance:
Instead of using inheritance to extend the behavior of an object, the decorator uses composition to “wrap” the object and add functionality. - Avoids Class Explosion:
Without the decorator pattern, you might end up with multiple subclasses for every possible combination of behaviors. The decorator pattern allows you to avoid this explosion of subclasses. - Enhancement of Behavior:
You can add new behaviors dynamically without modifying the base class.
When to Use the Decorator Pattern
- When you need to add functionality to objects without changing their classes.
For example, you can add features to a UI component or add responsibilities to an object in a flexible way. - When class inheritance is not feasible or desirable.
When you have a large number of subclasses to implement combinations of features, using decorators provides a cleaner solution. - When you want to add responsibilities to objects at runtime.
You can dynamically extend the behavior of objects by wrapping them with decorators at runtime, rather than creating a new subclass for each combination.
Advantages of the Decorator Pattern:
- Flexible and Reusable:
You can mix and match various decorators to achieve the desired functionality without modifying the underlying objects. - Avoids Class Hierarchy Explosion:
Instead of creating multiple subclasses for each combination of behaviors, you can create a chain of decorators. - Single Responsibility Principle (SRP):
Each decorator class has a single responsibility, allowing functionality to be added in a modular manner. - Easier to maintain:
You don’t need to modify existing classes to add new behaviors; decorators can be applied independently.
Disadvantages of the Decorator Pattern:
- Complexity:
With multiple decorators stacked on top of each other, the code might become harder to understand and maintain. - Multiple Objects:
The decorator pattern can lead to many small classes. Too many decorator objects may impact performance and make the code harder to manage.
Conclusion
The Decorator Pattern provides a powerful way to extend the behavior of an object without modifying its code. By using composition, it allows you to “decorate” or “wrap” objects with additional functionality in a flexible and reusable way. This is particularly useful when you need to add responsibilities to individual objects rather than entire classes, and it avoids the complexity of subclassing for every combination of features.