Decorator Pattern
Think of adding toppings to a pizza or filters to a photo: you start with something simple, then wrap it with extra behavior. You don’t need a new “class” of pizza for every possible combination.
The Decorator pattern is a structural pattern that lets you dynamically add responsibilities to objects by wrapping them in other objects. It’s a flexible alternative to subclassing for extending behavior.
Intent
Add new functionality to an object dynamically, without changing its class or affecting other instances, by wrapping it in decorators that implement the same interface.
Problem and Solution
Problem
Imagine a TextEditor that should support bold, italic, underline, strikethrough, links, and more. If you model each combination as a subclass, you quickly end up with a combinatorial explosion of types.
Solution
Decorator lets you keep a simple PlainText implementation and wrap it with decorators like BoldTextDecorator, ItalicTextDecorator, etc. Each decorator adds one behavior and forwards the rest. You can stack them in any order at runtime to compose behavior.
Structure
The Decorator pattern typically includes:
- Component Interface: Defines the interface for objects that can have responsibilities added to them dynamically.
- Concrete Component: The original object to which new functionality is added.
- Decorator: Abstract class that implements the component interface and contains a reference to a component.
- Concrete Decorators: Subclasses of the decorator that add specific behaviors to the component.
UML Diagram
+-------------------+ +-----------------------+
| Component |<----| Decorator |
|-------------------| |-----------------------|
| + operation() | | - component: Component|
+-------------------+ | + operation() |
^ +-----------------------+
| ^
| |
+------------------+ +----------------------+
|ConcreteComponent | | ConcreteDecorator |
|------------------| +----------------------+
| + operation() | | + operation() |
+------------------+ +----------------------+Example: Text Editor with Formatting
Let’s implement an example of a text editor using the Decorator pattern. We’ll have a basic TextEditor that can display plain text, and we’ll add decorators for different formatting options, such as bold and italic.
Step 1: Define the Component Interface
The Text interface defines the render method that all text components and decorators must implement.
// Component Interface
interface Text {
String render();
}Step 2: Implement the Concrete Component
The PlainText class implements the Text interface and represents the base component that can display plain text.
// Concrete Component
class PlainText implements Text {
private String content;
public PlainText(String content) {
this.content = content;
}
@Override
public String render() {
return content;
}
}Step 3: Create the Abstract Decorator
The TextDecorator class implements the Text interface and holds a reference to a Text object, allowing it to act as a wrapper.
// Decorator
abstract class TextDecorator implements Text {
protected Text text;
public TextDecorator(Text text) {
this.text = text;
}
@Override
public String render() {
return text.render();
}
}Step 4: Implement Concrete Decorators
Each concrete decorator adds specific behavior to the Text object, such as bold or italic formatting.
// Concrete Decorator for Bold Text
class BoldTextDecorator extends TextDecorator {
public BoldTextDecorator(Text text) {
super(text);
}
@Override
public String render() {
return "<b>" + text.render() + "</b>";
}
}
// Concrete Decorator for Italic Text
class ItalicTextDecorator extends TextDecorator {
public ItalicTextDecorator(Text text) {
super(text);
}
@Override
public String render() {
return "<i>" + text.render() + "</i>";
}
}Step 5: Client Code Using the Decorator
The client code can now create text with different combinations of decorators by wrapping the PlainText object with various decorators.
public class Client {
public static void main(String[] args) {
Text plainText = new PlainText("Hello, World!");
// Apply bold formatting
Text boldText = new BoldTextDecorator(plainText);
System.out.println(boldText.render()); // Output: <b>Hello, World!</b>
// Apply italic formatting
Text italicText = new ItalicTextDecorator(plainText);
System.out.println(italicText.render()); // Output: <i>Hello, World!</i>
// Apply both bold and italic formatting
Text boldItalicText = new BoldTextDecorator(new ItalicTextDecorator(plainText));
System.out.println(boldItalicText.render()); // Output: <b><i>Hello, World!</i></b>
}
}Explanation
In this example:
- The
Textinterface defines therendermethod that all text objects and decorators implement. PlainTextis the base component that displays plain text.BoldTextDecoratorandItalicTextDecoratorare concrete decorators that add bold and italic formatting, respectively.- The client can combine multiple decorators to create complex formatting dynamically.
Applicability
Use the Decorator pattern when:
- You need to add responsibilities to objects dynamically and flexibly.
- You want to avoid subclassing, as it would lead to a large number of classes for every possible combination of behaviors.
- You need to add different combinations of behaviors to objects without modifying the base class or the client code.
Advantages and Disadvantages
Advantages
- Flexible Behavior Extension: Decorator allows you to add or remove behaviors at runtime without modifying the original object.
- Adheres to Open-Closed Principle: New behaviors can be added by creating new decorators, rather than modifying existing classes.
- Combining Multiple Behaviors: You can stack multiple decorators around an object to create complex behaviors dynamically.
Disadvantages
- Increased Complexity: The Decorator pattern introduces additional classes, which may increase complexity, especially when many decorators are combined.
- Hard to Debug: Multiple layers of decorators can make it difficult to debug and trace the flow of execution.
- Order Sensitivity: The order in which decorators are applied can impact the final behavior, requiring careful attention.
Best Practices for Implementing the Decorator Pattern
- Use Composition Over Inheritance: Decorators should wrap objects instead of extending them, making the pattern more flexible.
- Limit Decorator Stacking: While multiple decorators can be stacked, avoid overuse, as this can lead to confusion and increased complexity.
- Use Naming Conventions: Clear naming for decorators helps in understanding the chain of decorators applied to an object.
Conclusion
The Decorator pattern provides a flexible and dynamic way to add behaviors to objects without modifying their structure. This pattern enables you to enhance the functionality of objects at runtime, creating a modular and extensible design that adheres to the Open-Closed Principle.