Moving into an unfurnished apartment requires adding flooring, lighting, and furniture to make it livable. Over time, you might swap pieces or introduce new amenities without rebuilding the walls. Software objects often face a similar need: capabilities must be layered onto core behavior at runtime without altering the underlying structure. The Decorator pattern solves this by transparently wrapping objects to inject additional responsibilities, offering a more adaptable alternative to subclassing.
Definition: Attach extra responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Structure and Participants
The pattern relies on four key roles:
- Component: An interface or abstract class defining the operations that may be altered.
- ConcreteComponent: The foundational object to which new behaviors are attached.
- Decorator: An abstract class implementing the Component interface while maintaining a reference to a Component instance.
- ConcreteDecorator: Specialized wrappers that add state or behavior before or after delegating to the wrapped object.
Implementation Example
Consider a file storage system where raw data may need to be compressed or encrypted transparently. The client writes plain text, yet the bytes on disk should optionally include both transformations.
// Component
public interface DataSink {
void write(String payload);
}
// ConcreteComponent
public class FileDataSink implements DataSink {
private String path;
public FileDataSink(String path) {
this.path = path;
}
@Override
public void write(String payload) {
System.out.println("Writing to " + path + ": " + payload);
}
}
// Decorator
public abstract class DataSinkWrapper implements DataSink {
protected DataSink inner;
public DataSinkWrapper(DataSink source) {
this.inner = source;
}
}
// ConcreteDecorator: Encryption
public class EncryptedSink extends DataSinkWrapper {
public EncryptedSink(DataSink source) {
super(source);
}
@Override
public void write(String payload) {
String encrypted = "ENC(" + payload + ")";
inner.write(encrypted);
}
}
// ConcreteDecorator: Compression
public class CompressedSink extends DataSinkWrapper {
public CompressedSink(DataSink source) {
super(source);
}
@Override
public void write(String payload) {
String compressed = "CMP[" + payload + "]";
inner.write(compressed);
}
}
Client usage demonstrates how decorators stack at runtime:
public class Application {
public static void main(String[] args) {
DataSink plain = new FileDataSink("backup.txt");
System.out.println("--- Raw output ---");
plain.write("Sensitive data");
DataSink compressed = new CompressedSink(plain);
System.out.println("--- With compression ---");
compressed.write("Sensitive data");
DataSink encryptedThenCompressed = new CompressedSink(new EncryptedSink(plain));
System.out.println("--- Encrypted then compressed ---");
encryptedThenCompressed.write("Sensitive data");
DataSink fullPipeline = new EncryptedSink(new CompressedSink(plain));
System.out.println("--- Compressed then encrypted ---");
fullPipeline.write("Sensitive data");
}
}
Output:
--- Raw output ---
Writing to backup.txt: Sensitive data
--- With compression ---
Writing to backup.txt: CMP[Sensitive data]
--- Encrypted then compressed ---
Writing to backup.txt: CMP[ENC(Sensitive data)]
--- Compressed then encrypted ---
Writing to backup.txt: ENC(CMP[Sensitive data])
Key Considerations
Several implementation details ensure the pattern works effectively:
First, both decorators and concrete components must share the same interface. This lets any decorator act as a substitute for the original object and enables nested wrapping.
Second, the decorator must hold a reference to the component it wraps, delegating operations downstream while injecting new logic around the call.
Third, composition replaces rigid inheritance hierarchies. Rather than predicting every feature combination upfront, individual capabilities remain separate and are mixed at runtime.
Fourth, each decorator should remain focused on a single augmentation. Small, cohesive wrappers maximize reuse and allow countless permutations when layered.
Advantages and Intent
The primary benefits flexibility: behavior is acquired by composiiton, so feature sets are assembled dynamically rather than hard-coded into class definitions. Existing code remains untouched, satisfying the Open/Closed principle. Core objects stay unmodified while their external interface gains power.
The pattern's purpose is to decompose complex feature sets into discrete, combinable units that can be wired together on demand. It simplifies otherwise bloated subclasses by distributing responsibilities across a chain of lightweight objects.
This approach resembles the Proxy pattern structurally, yet the intent differs. Decorators enhance functionality transparently, whereas proxies control access. Understanding that distinction helps select the right tool when similar wiring appears in code.