Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

Decorator Pattern 🎁

Overview

The Decorator Pattern allows behavior to be added to individual objects dynamically, either statically or at run-time, without affecting the behavior of other objects from the same class. This provides a flexible alternative to subclassing for extending functionality.

Difficulty: ⭐⭐ Medium
Category: Structural Pattern
Interview Frequency: ⭐⭐⭐⭐ Very Common

Problem It Solves

Imagine you have a base object and want to add features dynamically:

  • Without Decorator: Create 5 subclasses for 5 features, or 25 subclasses for combinations
  • With Decorator: Wrap the object with feature decorators

Example: Coffee shop with add-ons

Coffee
  ↓
Coffee + Milk → Coffee + Milk + Sugar → Coffee + Milk + Sugar + Whipped Cream

Real-World Analogy

Think of gifts and wrapping:

  • Core: A gift (book, watch, toy)
  • Decorators: Wrapping paper, ribbon, gift tag
  • You can combine any decorators without changing the core gift
  • Each wrapper adds functionality (protection, decoration) without modifying the gift itself

When to Use

Use Decorator Pattern when:

  • You want to add responsibilities to objects dynamically
  • Subclassing would create too many classes
  • You need to combine multiple features
  • You want to add features without modifying existing code
  • Features are independent and can be combined

Don't use when:

  • Features are core to the object (use inheritance)
  • Order of decorators matters critically
  • Simple scenarios (just add a property)
  • Performance is critical (wrapping adds overhead)

Implementation

See decorator.ts for complete implementations.

Key Components

  1. Component: Defines interface for objects that can be decorated
  2. ConcreteComponent: The base object
  3. Decorator: Abstract decorator implementing Component
  4. ConcreteDecorators: Add specific behaviors

TypeScript Example

// Component interface
interface Component {
  operation(): string;
}

// Concrete Component
class ConcreteComponent implements Component {
  operation(): string {
    return "ConcreteComponent";
  }
}

// Abstract Decorator
abstract class Decorator implements Component {
  constructor(protected component: Component) {}
  operation(): string {
    return this.component.operation();
  }
}

// Concrete Decorators
class ConcreteDecoratorA extends Decorator {
  operation(): string {
    return `ConcreteDecoratorA(${super.operation()})`;
  }
}

class ConcreteDecoratorB extends Decorator {
  operation(): string {
    return `ConcreteDecoratorB(${super.operation()})`;
  }
}

// Usage
let component: Component = new ConcreteComponent();
component = new ConcreteDecoratorA(component);
component = new ConcreteDecoratorB(component);

console.log(component.operation());
// Output: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))

Benefits

Flexibility: Add/remove features at runtime
Single Responsibility: Each decorator has one job
Composition over Inheritance: Avoid explosion of subclasses
Open/Closed Principle: Open for extension, closed for modification
Combination: Mix and match decorators in any order

Drawbacks

❌ Order of decorators can matter
❌ Hard to remove decorators from chain
❌ Performance overhead (wrapping)
❌ Complex to debug (many layers)
❌ Can lead to "decorator soup"

Interview Questions & Answers

Q1: Decorator vs Inheritance—which is better?

Answer: Decorator is better when:

  • You want to combine multiple features dynamically
  • You have many possible combinations (2^n subclasses needed)
  • Features are optional or vary at runtime

Example:

// Inheritance (Problem)
class Coffee {}
class CoffeeWithMilk extends Coffee {}
class CoffeeWithMilkAndSugar extends Coffee {}
class CoffeeWithMilkAndSugarAndWhippedCream extends Coffee {}
// ... exponential growth!

// Decorator (Solution)
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);

Q2: Decorator vs Strategy—what's the difference?

Answer:

  • Decorator: Adds behavior (wraps object) at runtime
  • Strategy: Swaps algorithm for context
// Decorator
class LoggingDecorator {
  constructor(private service: Service) {}
  execute() {
    console.log("Starting...");
    return this.service.execute();
  }
}

// Strategy
class Context {
  constructor(private strategy: Strategy) {}
  execute() {
    return this.strategy.execute();
  }
}

Decorator: "Add extra behavior"
Strategy: "Choose which algorithm"

Q3: How do you handle decorator order?

Answer: Order can matter:

// Different order, different results
const coffee1 = new MilkDecorator(new SugarDecorator(baseCoffee));
const coffee2 = new SugarDecorator(new MilkDecorator(baseCoffee));

// Results differ if price calculation depends on base
coffee1.getPrice(); // Milk(Sugar(base))
coffee2.getPrice(); // Sugar(Milk(base))

Solution: Make decorators order-independent or document the expected order.

Q4: Can you mix Decorator with other patterns?

Answer: Yes! Common combinations:

// Decorator + Factory
class DecoratorFactory {
  static createCoffee(options: string[]): Coffee {
    let coffee: Coffee = new SimpleCoffee();
    options.forEach((opt) => {
      coffee = this.getDecorator(opt, coffee);
    });
    return coffee;
  }
}

// Decorator + Strategy
class PaymentService {
  private strategy: PaymentStrategy;
  private decorators: Decorator[];

  execute() {
    let result = this.strategy.pay();
    this.decorators.forEach((d) => (result = d.decorate(result)));
    return result;
  }
}

Q5: How do you prevent decorator chains from getting too long?

Answer: Several strategies:

  1. Limit chain depth:
addDecorator(decorator: Decorator): void {
  if (this.decoratorCount >= 3) {
    throw new Error("Max decorators reached");
  }
  // ...
}
  1. Composite decorators:
// Combine multiple into one
class PremiumDecorator extends Decorator {
  constructor(component: Component) {
    super(new HealthBenefitsDecorator(new PriorityDecorator(component)));
  }
}
  1. Builder pattern:
new CoffeeBuilder().withMilk().withSugar().withWhippedCream().build();

Q6: TypeScript Decorators vs Pattern Decorators—what's the difference?

Answer:

  • TypeScript @decorator: Syntax feature (metadata, class/method modification)
  • Decorator Pattern: Design pattern (wrapping objects)

They're different things!

// TypeScript decorator (syntax)
@Logging
@Auth
class MyService {
  @Memoize
  expensiveMethod() {}
}

// Pattern decorator (runtime wrapping)
const service = new MyService();
const logged = new LoggingDecorator(service);
const secured = new AuthDecorator(logged);

Real-World Use Cases

  1. I/O Streams: BufferedInputStream, DataInputStream
  2. GUI Components: Borders, scrollbars, decorations
  3. Middleware: Express.js middleware stack
  4. Caching: Cache decorators for expensive operations
  5. Logging: Add logging to any service
  6. Authentication: Add auth checks to endpoints
  7. Compression: Add compression to data streams
  8. Validation: Add validation to input data
  9. Formatting: Add formatting to output
  10. Monitoring: Add metrics/telemetry

Code Smell Fixes

Before (Inheritance Explosion):

class User {}
class UserWithLogging extends User {}
class UserWithLoggingAndCaching extends User {}
class UserWithLoggingAndCachingAndAuth extends User {}
// ... nightmare!

After (Decorator Pattern):

let user: UserService = new UserRepository();
user = new LoggingDecorator(user);
user = new CachingDecorator(user);
user = new AuthDecorator(user);
// Clean and composable!

Common Patterns

1. Wrapper Pattern

class ServiceWrapper implements Service {
  constructor(private inner: Service) {}

  method(): Result {
    return this.inner.method();
  }
}

2. Conditional Decoration

if (enableLogging) {
  service = new LoggingDecorator(service);
}
if (enableCaching) {
  service = new CachingDecorator(service);
}

3. Dynamic Decoration

const config = loadConfig();
config.decorators.forEach((dec) => {
  service = decoratorFactory.create(dec, service);
});

Testing Tips

test("decorator should wrap component", () => {
  const component = new MockComponent();
  const decorated = new TestDecorator(component);

  decorated.operation();

  expect(component.operation).toHaveBeenCalled();
});

test("decorator should add behavior", () => {
  const component = new SimpleComponent();
  const decorated = new LoggingDecorator(component);

  const result = decorated.operation();

  expect(result).toContain("logged");
});

Related Patterns

  • Strategy: Swap algorithms; Decorator adds behavior
  • Proxy: Control access; Decorator adds features
  • Adapter: Change interface; Decorator enhances
  • Chain of Responsibility: Sequential processing
  • Builder: Construct complex objects

Summary

The Decorator Pattern solves the "explosion of subclasses" problem by:

  1. Wrapping objects instead of subclassing
  2. Adding behavior at runtime
  3. Combining features flexibly
  4. Keeping classes focused and reusable

Key Takeaway: "Wrap objects to add behavior without creating subclass explosion."

When to use: Multiple optional, combinable features
When not to: Core features (use inheritance), simple additions

Next: Try the Singleton Pattern