Appearance
Decorator
Dynamically add extra responsibilities to an object. Compared to generating subclasses, this approach is more flexible for extending functionality.
The Decorator pattern is a method for dynamically adding functionality to an instance of an object at runtime.
We’ve actually discussed the Decorator pattern in the section on the IO Filter pattern. In the Java standard library, InputStream
is an abstract class, and classes like FileInputStream
, ServletInputStream
, and Socket.getInputStream()
are final data sources.
If we want to add functionalities like buffering, signature calculation, or encryption/decryption to these data sources, we would need a total of 9 subclasses (3 final data sources × 3 functionalities). This would lead to a combinatorial explosion of subclasses, which is clearly not a viable design.
The purpose of the Decorator pattern is to layer additional functionalities onto the original data source using decorators, allowing us to achieve the desired functionality through composition.
For example, to add buffering and decompression features to FileInputStream
, we can implement it as follows:
java
// Create the original data source:
InputStream fis = new FileInputStream("test.gz");
// Add buffering functionality:
InputStream bis = new BufferedInputStream(fis);
// Add decompression functionality:
InputStream gis = new GZIPInputStream(bis);
Or, we can write it all at once:
java
InputStream input = new GZIPInputStream( // Second layer of decoration
new BufferedInputStream( // First layer of decoration
new FileInputStream("test.gz") // Core functionality
));
Notice that BufferedInputStream
and GZIPInputStream
both inherit from FilterInputStream
, which serves as an abstract decorator. Here’s a diagram representing the Decorator pattern:
┌───────────┐
│ Component │
└───────────┘
▲
┌────────────┼─────────────────┐
│ │ │
┌───────────┐┌───────────┐ ┌───────────┐
│ComponentA ││ComponentB │... │ Decorator │
└───────────┘└───────────┘ └───────────┘
▲
┌──────┴──────┐
│ │
┌───────────┐ ┌───────────┐
│DecoratorA │ │DecoratorB │...
└───────────┘ └───────────┘
The top-level Component
is the interface, corresponding to the abstract class InputStream
in IO. ComponentA
and ComponentB
are actual subclasses, corresponding to data sources like FileInputStream
and ServletInputStream
. The Decorator
serves as an abstract decorator that implements various additional functionalities, corresponding to FilterInputStream
in IO. Each class derived from Decorator
represents a specific decorator, such as BufferedInputStream
or GZIPInputStream
.
Benefits of the Decorator Pattern
The Decorator pattern effectively separates core functionality from additional functionality. The core functionality refers to data sources like FileInputStream
that actually read data, while additional functionality includes buffering, compression, and decryption. If we want to add core functionality, we create a new subclass of Component
, such as ByteInputStream
. If we want to add additional functionality, we create a new subclass of Decorator
, such as CipherInputStream
. Both parts can be independently extended, allowing the caller to freely compose additional functionalities, greatly enhancing flexibility.
Designing a Complete Decorator Pattern
Let’s take an example where we need to render HTML text, with additional effects like bold, italic, and underline. To achieve dynamic effects, we can use the Decorator pattern.
First, we need to define the top-level interface TextNode
:
java
public interface TextNode {
// Set text:
void setText(String text);
// Get text:
String getText();
}
For a core node, such as <span>
, it should directly inherit from TextNode
:
java
public class SpanNode implements TextNode {
private String text;
public void setText(String text) {
this.text = text;
}
public String getText() {
return "<span>" + text + "</span>";
}
}
Next, to implement the Decorator pattern, we need an abstract Decorator
class:
java
public abstract class NodeDecorator implements TextNode {
protected final TextNode target;
protected NodeDecorator(TextNode target) {
this.target = target;
}
public void setText(String text) {
this.target.setText(text);
}
}
The NodeDecorator
class holds a reference to a TextNode
, which is the instance to which we will add functionality. We can now implement a bold functionality:
java
public class BoldDecorator extends NodeDecorator {
public BoldDecorator(TextNode target) {
super(target);
}
public String getText() {
return "<b>" + target.getText() + "</b>";
}
}
Similar classes can be created for ItalicDecorator
, UnderlineDecorator
, etc. The client can freely combine these decorators:
java
TextNode n1 = new SpanNode();
TextNode n2 = new BoldDecorator(new UnderlineDecorator(new SpanNode()));
TextNode n3 = new ItalicDecorator(new BoldDecorator(new SpanNode()));
n1.setText("Hello");
n2.setText("Decorated");
n3.setText("World");
System.out.println(n1.getText());
// Output: <span>Hello</span>
System.out.println(n2.getText());
// Output: <b><u><span>Decorated</span></u></b>
System.out.println(n3.getText());
// Output: <i><b><span>World</span></b></i>
Exercise
Use the Decorator pattern to add a <del>
tag to represent deletion.
Summary
Using the Decorator pattern, you can independently increase core functionality and additional functionality, with neither affecting the other. You can dynamically add any number of additional functionalities to core functionality at runtime.