Table of Contents
What is decorator pattern?
The decorator pattern is a structural design pattern that allows behaviour to be added to individual objects, either statically or dynamically, without affecting the behaviour of other objects from the same class. Here is a diagram to show the decorator pattern.
When is decorator pattern used?
This pattern is particularly useful in scenarios where you need to augment the functionalities of objects at runtime and when extending functionality by subclassing would be impractical or lead to a large number of subclasses. Here are some specific situations when using the decorator pattern is particularly beneficial:
- If you need to add new functionalities to an object dynamically and transparently—that is, without affecting other objects of the same class—the decorator pattern is a good choice. This can be useful in user interface components or logging functionalities where behaviors can be turned on and off dynamically.
- In legacy systems or third-party class libraries, you often cannot modify the source code directly. The decorator pattern allows you to extend the functionality of these classes without changing the original code.
- If subclassing would lead to an explosion of new classes to cover every possible combination of functionalities, using decorators can avoid this subclass proliferation. For instance, if you have several different types of embellishments to add to a basic UI component, using decorators can be more scalable than creating a new subclass for every combination.
- When you need to prevent code duplication: Instead of having repetitive code across multiple subclasses, decorators allow creating one reusable class that modifies the behavior. This leads to cleaner and more maintainable code.
- The decorator pattern provides more control over how and when to extend an object’s behaviour. Because you apply each decorator individually, you can selectively apply extensions without affecting other instances of the class.
Example Scenario
Consider a text rendering system where you might want to sometimes add scrolling, bordering, or shadow effects to certain text blocks but not others. Implementing each of these capabilities as a subclass would lead to a significant number of combinations (e.g., TextWithBorder, TextWithShadow, TextWithBorderAndShadow, etc.). Using decorators, you can create a simple Text class and dynamically attach the decorations like Border, Shadow, and Scrolling as needed.
In summary, the decorator pattern is a valuable tool for adding functionality to objects dynamically while keeping the system flexible and maintainable. It is especially useful when dealing with the extension of capabilities in a scalable and non-intrusive manner. The decorator pattern is especially popular in software development for GUI toolkits where components may need to be dynamically enhanced with new behaviors (like scrolling, bordering, or coloring), and in various middleware frameworks where systematic enhancements (like logging, security checks, or transaction handling) are essential.
Why to use decorator pattern?
The decorator pattern is used for several key reasons that address common software design challenges, particularly when it comes to extending functionality in a flexible and scalable manner. Here’s a detailed breakdown of why the decorator pattern is so useful:
- Dynamic Extension of Responsibilities: One of the primary reasons to use the decorator pattern is to add new functionalities to objects dynamically. Unlike static inheritance, where functionality is embedded directly into an object’s class and affects all instances, the decorator pattern allows you to add behavior at runtime to individual objects without impacting others.
- Avoiding Subclass Explosion: In complex systems, extending functionalities through subclasses can lead to a rapid increase in the number of classes, making the system hard to understand and maintain. The decorator pattern allows functionalities to be added in a more granular fashion without creating a new subclass for each combination of features.
- Maintaining Class Responsibilities: The decorator pattern helps keep classes focused on their primary responsibilities. Rather than compounding a class with multiple functionalities (e.g., formatting, validation, logging), decorators allow these to be added as needed through separate classes. This adheres to the Single Responsibility Principle, one of the SOLID principles in software design, which promotes greater modularity and simpler maintenance.
- Enhancing Code Reusability: Decorators can be mixed and matched and reused in different contexts. This makes the decorator pattern very powerful for libraries where operations like logging, monitoring, or data transformations need to be applied across various parts of an application.
- Flexibility in Adding or Removing Features: With the decorator pattern, features can be added or removed at runtime just by attaching or detaching decorators. This is particularly useful in scenarios where behavior may need to be customized according to the context of execution.
- Transparency in Operation: Decorators can add functionality while keeping the interface consistent. This means objects using decorated components do not need to be aware of whether they are working with a decorated or a plain object, which simplifies the interaction model.
- Ease of Combining Behaviors: Since each decorator class handles only one enhancement, combining multiple behaviors becomes straightforward and intuitive. This allows developers to layer additional functionalities efficiently.
How to implement decorator pattern (in Java)
Let’s consider an example in a text processing context, where various formatting options are applied to text, such as bolding, italicizing, underlining, and adding a shadow. I’ll first demonstrate how this might be implemented without using the decorator pattern, leading to a class explosion problem. Then, I’ll show how the decorator pattern resolves these issues by allowing more flexible and maintainable code.
Without decorator pattern
interface Text {
String getContent();
}
class PlainText implements Text {
private String content;
public PlainText(String content) {
this.content = content;
}
@Override
public String getContent() {
return content;
}
}
class BoldText implements Text {
private Text text;
public BoldText(Text text) {
this.text = text;
}
@Override
public String getContent() {
return "<b>" + text.getContent() + "</b>";
}
}
class ItalicText implements Text {
private Text text;
public ItalicText(Text text) {
this.text = text;
}
@Override
public String getContent() {
return "<i>" + text.getContent() + "</i>";
}
}
class UnderlineText implements Text {
private Text text;
public UnderlineText(Text text) {
this.text = text;
}
@Override
public String getContent() {
return "<u>" + text.getContent() + "</u>";
}
}
class BoldItalicText implements Text {
private Text text;
public BoldItalicText(Text text) {
this.text = text;
}
@Override
public String getContent() {
return "<b><i>" + text.getContent() + "</i></b>";
}
}
// And so on for other combinations like BoldUnderlineText, ItalicUnderlineText, BoldItalicUnderlineText, etc.
Caller code:
Usage Example for Non-Decorator Pattern Code
Let’s assume you want to create a text that is both bold and italic. Here’s how you would instantiate and use the classes to achieve this:
public class Main {
public static void main(String[] args) {
Text plainText = new PlainText("Hello, world!");
Text boldText = new BoldText(plainText);
Text boldItalicText = new BoldItalicText(boldText);
System.out.println(boldItalicText.getContent());
}
}
This example would output HTML-like formatted text indicating that the text “Hello, world!” has been formatted to be bold and italic.
Now, suppose you want to create text that is bold, italic, and underlined. Since we do not have a class for all three combinations (BoldItalicUnderlineText), you can’t directly apply all three styles without creating a new class specifically for this combination. However, for the purpose of this example, if such a class were defined, here’s how it might be used:
public class Main {
public static void main(String[] args) {
Text plainText = new PlainText("Hello, world!");
Text boldItalicUnderlineText = new BoldItalicUnderlineText(plainText);
System.out.println(boldItalicUnderlineText.getContent());
}
}
Defining a new combined class
Let’s also quickly define what the BoldItalicUnderlineText
class might look like, continuing from the earlier set of class definitions, showing the rigidness and complexity of adding new combined functionality:
class BoldItalicUnderlineText implements Text {
private Text text;
public BoldItalicUnderlineText(Text text) {
this.text = text;
}
@Override
public String getContent() {
return "<b><i><u>" + text.getContent() + "</u></i></b>";
}
}
Analysis of the Non-Decorator Approach
This example highlights several issues:
- Rigidity: You need to predefine and hardcode every possible combination of styles, making the system less adaptable and more cumbersome to maintain.
- Scalability Issues: Every time a new style is introduced, the number of classes can grow exponentially to cover all possible combinations.
- Code Duplication: Each new class repeats the logic of applying styles, which can lead to a significant amount of redundancy.
This approach is quite limiting compared to the flexibility offered by the decorator pattern, where you can dynamically add or remove features without altering the existing infrastructure and without the need for an extensive class hierarchy.
Using Decorator pattern
interface Text {
String getContent();
}
class PlainText implements Text {
private String content;
public PlainText(String content) {
this.content = content;
}
@Override
public String getContent() {
return content;
}
}
abstract class TextDecorator implements Text {
protected Text text;
public TextDecorator(Text text) {
this.text = text;
}
}
class BoldDecorator extends TextDecorator {
public BoldDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<b>" + text.getContent() + "</b>";
}
}
class ItalicDecorator extends TextDecorator {
public ItalicDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<i>" + text.getContent() + "</i>";
}
}
class UnderlineDecorator extends TextDecorator {
public UnderlineDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<u>" + text.getContent() + "</u>";
}
}
class ShadowDecorator extends TextDecorator {
public ShadowDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<shadow>" + text.getContent() + "</shadow>";
}
}
Caller Code
public class Main {
public static void main(String[] args) {
Text plainText = new PlainText("Hello, world!");
Text boldItalic = new ItalicDecorator(new BoldDecorator(plainText));
Text boldItalicUnderline = new UnderlineDecorator(boldItalic);
Text fullDecorated = new ShadowDecorator(boldItalicUnderline);
System.out.println(fullDecorated.getContent());
}
}