Design Principle
Decorator Pattern
Learn the Decorator pattern: attach additional responsibilities to objects dynamically. Provide a flexible alternative to subclassing for extending functionality.
The Decorator Pattern attaches additional responsibilities to objects dynamically. It provides a flexible alternative to subclassing for extending functionality.
What is the Decorator Pattern?
Decorator Pattern provides:
- Dynamic behavior: Add behavior at runtime
- Composition over inheritance: Extend without subclassing
- Flexible combination: Combine multiple decorators
- Single responsibility: Each decorator adds one feature
Use cases:
- Adding features to objects dynamically
- Extending functionality without modifying classes
- Combining multiple features
- Stream processing (Java I/O streams)
Structure
Component (interface)
└─ operation()
ConcreteComponent (implements Component)
└─ operation()
Decorator (implements Component)
└─ component: Component
└─ operation() (calls component.operation())
ConcreteDecorator (extends Decorator)
└─ operation() (adds behavior, calls super.operation())
Examples
Basic Decorator Pattern
// Component interface
interface Coffee {
getCost(): number;
getDescription(): string;
}
// Concrete component
class SimpleCoffee implements Coffee {
getCost(): number {
return 5;
}
getDescription(): string {
return "Simple coffee";
}
}
// Decorator
abstract class CoffeeDecorator implements Coffee {
protected coffee: Coffee;
constructor(coffee: Coffee) {
this.coffee = coffee;
}
getCost(): number {
return this.coffee.getCost();
}
getDescription(): string {
return this.coffee.getDescription();
}
}
// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
getCost(): number {
return this.coffee.getCost() + 2;
}
getDescription(): string {
return this.coffee.getDescription() + ", milk";
}
}
class SugarDecorator extends CoffeeDecorator {
getCost(): number {
return this.coffee.getCost() + 1;
}
getDescription(): string {
return this.coffee.getDescription() + ", sugar";
}
}
class WhipDecorator extends CoffeeDecorator {
getCost(): number {
return this.coffee.getCost() + 3;
}
getDescription(): string {
return this.coffee.getDescription() + ", whip";
}
}
// Usage
let coffee: Coffee = new SimpleCoffee();
console.log(coffee.getDescription()); // Simple coffee
console.log(coffee.getCost()); // 5
coffee = new MilkDecorator(coffee);
console.log(coffee.getDescription()); // Simple coffee, milk
console.log(coffee.getCost()); // 7
coffee = new SugarDecorator(coffee);
console.log(coffee.getDescription()); // Simple coffee, milk, sugar
console.log(coffee.getCost()); // 8
coffee = new WhipDecorator(coffee);
console.log(coffee.getDescription()); // Simple coffee, milk, sugar, whip
console.log(coffee.getCost()); // 11
HTTP Request Decorator
// Component
interface HTTPRequest {
send(): Promise<Response>;
}
// Concrete component
class BasicHTTPRequest implements HTTPRequest {
constructor(private url: string, private options: RequestInit) {}
async send(): Promise<Response> {
return await fetch(this.url, this.options);
}
}
// Decorator
abstract class RequestDecorator implements HTTPRequest {
protected request: HTTPRequest;
constructor(request: HTTPRequest) {
this.request = request;
}
async send(): Promise<Response> {
return await this.request.send();
}
}
// Concrete decorators
class AuthDecorator extends RequestDecorator {
constructor(request: HTTPRequest, private token: string) {
super(request);
}
async send(): Promise<Response> {
// Add authentication header
const originalOptions = (this.request as BasicHTTPRequest)['options'];
const newOptions = {
...originalOptions,
headers: {
...originalOptions.headers,
'Authorization': `Bearer ${this.token}`
}
};
const decoratedRequest = new BasicHTTPRequest(
(this.request as BasicHTTPRequest)['url'],
newOptions
);
return await decoratedRequest.send();
}
}
class RetryDecorator extends RequestDecorator {
constructor(request: HTTPRequest, private maxRetries: number = 3) {
super(request);
}
async send(): Promise<Response> {
let lastError: Error | null = null;
for (let i = 0; i < this.maxRetries; i++) {
try {
return await this.request.send();
} catch (error) {
lastError = error as Error;
if (i < this.maxRetries - 1) {
await this.delay(1000 * (i + 1)); // Exponential backoff
}
}
}
throw lastError || new Error("Request failed");
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
class LoggingDecorator extends RequestDecorator {
async send(): Promise<Response> {
console.log(`Sending request...`);
const startTime = Date.now();
try {
const response = await this.request.send();
const duration = Date.now() - startTime;
console.log(`Request completed in ${duration}ms`);
return response;
} catch (error) {
const duration = Date.now() - startTime;
console.error(`Request failed after ${duration}ms:`, error);
throw error;
}
}
}
// Usage
let request: HTTPRequest = new BasicHTTPRequest("https://api.example.com/data", {
method: "GET"
});
// Add decorators
request = new AuthDecorator(request, "token123");
request = new RetryDecorator(request, 3);
request = new LoggingDecorator(request);
const response = await request.send();
Common Pitfalls
- Too many decorators: Can become complex. Fix: Keep decorators simple, limit nesting
- Order matters: Decorator order affects behavior. Fix: Document order, use builder pattern
- Performance overhead: Multiple layers add overhead. Fix: Consider performance impact
- Not using interfaces: Tight coupling. Fix: Use interfaces, dependency injection
Interview Questions
Beginner
Q: What is the Decorator pattern and how does it differ from inheritance?
A:
Decorator Pattern attaches additional responsibilities to objects dynamically.
Key characteristics:
- Dynamic behavior: Add behavior at runtime
- Composition: Uses composition instead of inheritance
- Flexible combination: Combine multiple decorators
- Single responsibility: Each decorator adds one feature
Example:
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
// Coffee now has milk and sugar
Difference from inheritance:
- Inheritance: Static, compile-time, creates new class
- Decorator: Dynamic, runtime, wraps existing object
Benefits:
- Flexibility: Add/remove features at runtime
- No class explosion: Don't need classes for every combination
- Single responsibility: Each decorator does one thing
Intermediate
Q: Explain how the Decorator pattern works. How do you compose multiple decorators?
A:
Decorator Pattern Structure:
1. Component Interface:
interface Component {
operation(): void;
}
2. Concrete Component:
class ConcreteComponent implements Component {
operation(): void {
console.log("Base operation");
}
}
3. Decorator (Abstract):
abstract class Decorator implements Component {
protected component: Component;
constructor(component: Component) {
this.component = component;
}
operation(): void {
this.component.operation();
}
}
4. Concrete Decorators:
class ConcreteDecoratorA extends Decorator {
operation(): void {
super.operation();
this.addedBehavior();
}
private addedBehavior(): void {
console.log("Added behavior A");
}
}
Composing Multiple Decorators:
let component: Component = new ConcreteComponent();
component = new ConcreteDecoratorA(component);
component = new ConcreteDecoratorB(component);
component = new ConcreteDecoratorC(component);
component.operation();
// Output:
// Base operation
// Added behavior A
// Added behavior B
// Added behavior C
Order matters: Decorators are applied in reverse order of wrapping.
Senior
Q: Design a decorator system for a text processing pipeline that supports multiple transformations (encryption, compression, formatting) that can be applied in any order. Handle performance and ensure transformations are reversible.
A:
// Component interface
interface TextProcessor {
process(text: string): string;
reverse(text: string): string; // For reversible transformations
}
// Concrete component
class PlainTextProcessor implements TextProcessor {
process(text: string): string {
return text;
}
reverse(text: string): string {
return text;
}
}
// Decorator
abstract class TextProcessorDecorator implements TextProcessor {
protected processor: TextProcessor;
constructor(processor: TextProcessor) {
this.processor = processor;
}
process(text: string): string {
return this.processor.process(text);
}
reverse(text: string): string {
return this.processor.reverse(text);
}
}
// Encryption decorator
class EncryptionDecorator extends TextProcessorDecorator {
private key: string;
constructor(processor: TextProcessor, key: string) {
super(processor);
this.key = key;
}
process(text: string): string {
const processed = this.processor.process(text);
return this.encrypt(processed);
}
reverse(text: string): string {
const decrypted = this.decrypt(text);
return this.processor.reverse(decrypted);
}
private encrypt(text: string): string {
// Encryption logic
return btoa(text); // Simple base64 for example
}
private decrypt(text: string): string {
// Decryption logic
return atob(text);
}
}
// Compression decorator
class CompressionDecorator extends TextProcessorDecorator {
process(text: string): string {
const processed = this.processor.process(text);
return this.compress(processed);
}
reverse(text: string): string {
const decompressed = this.decompress(text);
return this.processor.reverse(decompressed);
}
private compress(text: string): string {
// Compression logic (simplified)
return text; // Would use actual compression
}
private decompress(text: string): string {
// Decompression logic
return text;
}
}
// Formatting decorator
class FormattingDecorator extends TextProcessorDecorator {
process(text: string): string {
const processed = this.processor.process(text);
return this.format(processed);
}
reverse(text: string): string {
const unformatted = this.unformat(text);
return this.processor.reverse(unformatted);
}
private format(text: string): string {
return text.toUpperCase();
}
private unformat(text: string): string {
return text.toLowerCase();
}
}
// Pipeline builder
class TextProcessingPipeline {
private processor: TextProcessor;
constructor() {
this.processor = new PlainTextProcessor();
}
addEncryption(key: string): TextProcessingPipeline {
this.processor = new EncryptionDecorator(this.processor, key);
return this;
}
addCompression(): TextProcessingPipeline {
this.processor = new CompressionDecorator(this.processor);
return this;
}
addFormatting(): TextProcessingPipeline {
this.processor = new FormattingDecorator(this.processor);
return this;
}
process(text: string): string {
return this.processor.process(text);
}
reverse(text: string): string {
return this.processor.reverse(text);
}
}
// Usage
const pipeline = new TextProcessingPipeline()
.addFormatting()
.addEncryption("secret-key")
.addCompression();
const processed = pipeline.process("Hello World");
const reversed = pipeline.reverse(processed);
Features:
- Reversible transformations: Each decorator can reverse
- Flexible composition: Add decorators in any order
- Performance: Can optimize based on decorator order
- Pipeline builder: Fluent interface for building pipelines
Key Takeaways
- Decorator pattern: Adds behavior to objects dynamically
- Composition over inheritance: Extends without subclassing
- Flexible combination: Combine multiple decorators
- Single responsibility: Each decorator adds one feature
- Use cases: Adding features dynamically, stream processing, text processing
- Best practices: Keep decorators simple, document order, consider performance
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.