Decorator Pattern
Decorator is structural design pattern that allows dynamically adding new responsibilities to object by wrapping it in "decorator" objects. At same time original object isn't changed, and additional logic is "mixed in" from outside.
Why Decorator is Needed:
Decorator pattern allows flexibly extending object functionality without changing their code. This is especially convenient if inheritance cannot (or shouldn't) be used.
When to Apply "Decorator"?
1. Dynamic Extension
When need to add new capabilities to objects "on the fly" without creating many subclasses.
2. Inheritance Limitations
When impossible or undesirable to modify class directly (e.g., classes from third-party library).
3. Multiple Intersecting Features
When several types of functionality need to be combined differently. Decorators allow combining features without creating explosive number of subclasses.
How It Works
- Base interface or abstract class is defined that describes object's main behavior.
- Concrete class implements this behavior.
- Decorator also implements same base interface, contains reference to object of same type and delegates work to it, while adding own behavior "before" or "after" main logic.
Thus, decorators can wrap each other, creating chain of objects — each adds its features.
Example (TypeScript)
// 1. Base interface for components
interface DataSource {
writeData(data: string): void;
readData(): string;
}
// 2. Concrete class implementing interface
class FileDataSource implements DataSource {
private filename: string;
private buffer: string;
constructor(filename: string) {
this.filename = filename;
this.buffer = "";
}
writeData(data: string): void {
// Logic for writing data to file
this.buffer = data;
console.log(`Data "${data}" written to file: ${this.filename}`);
}
readData(): string {
// Logic for reading data from file
console.log(`Reading data from file: ${this.filename}`);
return this.buffer;
}
}
// 3. Common decorator class
class DataSourceDecorator implements DataSource {
protected wrappee: DataSource;
constructor(source: DataSource) {
this.wrappee = source;
}
writeData(data: string): void {
this.wrappee.writeData(data);
}
readData(): string {
return this.wrappee.readData();
}
}
// 4. Concrete decorators
class EncryptionDecorator extends DataSourceDecorator {
writeData(data: string): void {
const encrypted = btoa(data); // simple Base64 "encryption"
console.log("Encrypting data...");
super.writeData(encrypted);
}
readData(): string {
const data = super.readData();
console.log("Decrypting data...");
return atob(data);
}
}
class CompressionDecorator extends DataSourceDecorator {
writeData(data: string): void {
console.log("Compressing data...");
const compressed = `COMPRESSED(${data})`;
super.writeData(compressed);
}
readData(): string {
const data = super.readData();
console.log("Decompressing data...");
return data.replace(/^COMPRESSED\(|\)$/g, "");
}
}
// 5. Client code
function clientCode() {
const file = new FileDataSource("data.txt");
// Wrap file with decorators
const encryption = new EncryptionDecorator(file);
const compression = new CompressionDecorator(encryption);
// Write
compression.writeData("Hello, Decorator!");
// Read
console.log("Final data:", compression.readData());
}
clientCode();
- FileDataSource — base implementation working with file.
- DataSourceDecorator — common decorator class that accepts DataSource and implements same interface.
- EncryptionDecorator and CompressionDecorator add their logic by wrapping writeData() and readData() calls.
Advantages
- Flexibility: can add functionality to objects on the fly, creating various decorator combinations.
- Single Responsibility Principle (SRP): each new behavior is formalized as separate decorator class.
- Reuse: one decorator can be applied to different objects without affecting their internal code.
Disadvantages
- Structure complexity: With large number of decorators chain can become cumbersome to understand.
- Debugging complexity: need to watch wrapping order, as final behavior depends on decorator sequence.
Important:
If you use too many decorators, this can complicate operation sequence logic. Evaluate how much dynamics and flexibility is really needed before introducing Decorator pattern.
Read more about Decorator here.
- refactoring.guru - patterns and refactoring
- patterns.dev - patterns