Low-level design3 min

Decorator Pattern

Type: Structural Pattern
Relevance: High. Extremely common in web frameworks (e.g., Python decorators, TypeScript decorators, middleware).

The Decorator pattern lets you attach new behaviors to objects dynamically by placing these objects inside special wrapper objects that contain the behaviors.

Real-Life Analogy

Think of ordering a Coffee. You start with a BasicCoffee. You want milk? You don't create a new subclass CoffeeWithMilk. Instead, you wrap your coffee in a MilkDecorator. You want sugar? You wrap it again in a SugarDecorator. The final price is calculated recursively: Sugar(Milk(BasicCoffee)).getCost(). You dynamically added features at runtime without creating a massive subclass hierarchy.

The Problem

If you want to alter the behavior of an object, the first instinct is to extend its class. But inheritance is static (you can't alter behavior at runtime) and you can only inherit from one parent class. This leads to class explosion (e.g., Notifier, EmailNotifier, SMSNotifier, SlackNotifier, EmailAndSMSNotifier...).

How to Implement Decorator

  1. Create an interface that both the concrete component and the decorators will implement.
  2. Create a Base Decorator class that holds a reference to a wrapped component.
  3. Create Concrete Decorators that extend the base decorator and add specific behavior before or after calling the wrapped component's methods.

Example in Code

typescript
// 1. The Component Interface
interface DataSource {
  writeData(data: string): void;
  readData(): string;
}

// 2. The Concrete Component
class FileDataSource implements DataSource {
  private fileData: string = "";

  writeData(data: string): void {
    console.log(`Writing data to file: ${data}`);
    this.fileData = data;
  }

  readData(): string {
    return this.fileData;
  }
}

// 3. The Base Decorator
class DataSourceDecorator implements DataSource {
  protected wrappee: DataSource;

  constructor(source: DataSource) {
    this.wrappee = source;
  }

  writeData(data: string): void {
    this.wrappee.writeData(data); // Delegate by default
  }

  readData(): string {
    return this.wrappee.readData(); // Delegate by default
  }
}

// 4. Concrete Decorators
class EncryptionDecorator extends DataSourceDecorator {
  writeData(data: string): void {
    const encrypted = `ENCRYPTED[${data}]`;
    console.log("Encrypting data...");
    super.writeData(encrypted);
  }

  readData(): string {
    const data = super.readData();
    console.log("Decrypting data...");
    return data.replace("ENCRYPTED[", "").replace("]", "");
  }
}

class CompressionDecorator extends DataSourceDecorator {
  writeData(data: string): void {
    const compressed = `COMPRESSED(${data})`;
    console.log("Compressing data...");
    super.writeData(compressed);
  }

  readData(): string {
    const data = super.readData();
    console.log("Decompressing data...");
    return data.replace("COMPRESSED(", "").replace(")", "");
  }
}

// Client Code
const rawData = "Secret Password";

// We can dynamically wrap the file with both compression AND encryption!
let source: DataSource = new FileDataSource();
source = new CompressionDecorator(source);
source = new EncryptionDecorator(source);

source.writeData(rawData);
// Output: 
// Encrypting data...
// Compressing data...
// Writing data to file: COMPRESSED(ENCRYPTED[Secret Password])

Class Diagram

Why is it Important?

  • Single Responsibility: You can divide a monolithic class that implements many possible variants of behavior into several smaller classes.
  • Runtime Flexibility: You can add or remove responsibilities from an object at runtime.