← Back to principles

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:

  1. Reversible transformations: Each decorator can reverse
  2. Flexible composition: Add decorators in any order
  3. Performance: Can optimize based on decorator order
  4. 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.