4. How would you implement the Decorator pattern to add new functionality to an existing class without altering its structure?

Advanced

4. How would you implement the Decorator pattern to add new functionality to an existing class without altering its structure?

Overview

The Decorator pattern is a structural design pattern that allows for the addition of new functionality to an existing object without altering its structure. This is achieved by creating a set of decorator classes that are used to wrap the original class instances, thereby providing a flexible alternative to subclassing for extending functionality. It is particularly important in scenarios where modifying the original class is impractical or impossible due to its being part of a library or system you do not have control over.

Key Concepts

  1. Composition over Inheritance: The Decorator pattern emphasizes using composition to extend an object's behavior dynamically, rather than inheritance. This approach provides more flexibility in adding or removing responsibilities from objects at runtime.
  2. Open/Closed Principle: Decorators adhere to the open/closed principle, one of the SOLID principles of object-oriented design, which states that a class should be open for extension but closed for modification.
  3. Wrapper: A decorator acts as a wrapper to the original class. It can execute its own behavior either before or after delegating the call to the object it decorates.

Common Interview Questions

Basic Level

  1. What is the Decorator pattern and when would you use it?
  2. Can you demonstrate a simple implementation of the Decorator pattern?

Intermediate Level

  1. How does the Decorator pattern differ from subclassing?

Advanced Level

  1. How would you implement multiple decorators for a single class?

Detailed Answers

1. What is the Decorator pattern and when would you use it?

Answer: The Decorator pattern is a structural design pattern that allows for dynamically adding new functionality to objects without altering their structure by wrapping them with new functionalities. It is used when there is a need to add responsibilities to individual objects without affecting other objects or when extending functionality through inheritance is impractical due to the static nature of the language or the necessity to extend functionality across several classes.

Key Points:
- Provides a flexible alternative to subclassing for extending functionality.
- Helps in adhering to the open/closed principle.
- It’s a way to add responsibilities to objects dynamically.

Example:

public interface ICoffee
{
    string GetDescription();
    double GetCost();
}

// Concrete Component
public class SimpleCoffee : ICoffee
{
    public string GetDescription() => "Simple Coffee";
    public double GetCost() => 5;
}

// Decorator
public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee _coffee;
    public CoffeeDecorator(ICoffee coffee)
    {
        _coffee = coffee;
    }

    public virtual string GetDescription() => _coffee.GetDescription();
    public virtual double GetCost() => _coffee.GetCost();
}

// Concrete Decorator
public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() => _coffee.GetDescription() + ", Milk";
    public override double GetCost() => _coffee.GetCost() + 1.5;
}

2. Can you demonstrate a simple implementation of the Decorator pattern?

Answer: Below is a simple implementation of the Decorator pattern using a Coffee example, where we start with a simple coffee and then extend its functionality by decorating it with milk and then with sugar without changing the SimpleCoffee class.

Key Points:
- Decorators can be nested to add multiple layers of functionality.
- Each decorator adds its own behavior either before or after delegating to the wrapped object.
- The client interacts with the interface, allowing decorators to be transparently used.

Example:

// Another Concrete Decorator
public class SugarDecorator : CoffeeDecorator
{
    public SugarDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() => _coffee.GetDescription() + ", Sugar";
    public override double GetCost() => _coffee.GetCost() + 0.5;
}

// Using decorators
var myCoffee = new SimpleCoffee();
Console.WriteLine($"{myCoffee.GetDescription()} Cost: {myCoffee.GetCost()}");

myCoffee = new MilkDecorator(myCoffee);
Console.WriteLine($"{myCoffee.GetDescription()} Cost: {myCoffee.GetCost()}");

myCoffee = new SugarDecorator(myCoffee);
Console.WriteLine($"{myCoffee.GetDescription()} Cost: {myCoffee.GetCost()}");

3. How does the Decorator pattern differ from subclassing?

Answer: The Decorator pattern provides a more flexible way to add functionality to objects than subclassing. In subclassing, functionality is added at compile time by creating a static inheritance hierarchy, whereas the Decorator pattern allows for adding functionality dynamically at runtime through composition and delegation. This means that with decorators, functionality can be added to specific instances of classes without affecting other instances or requiring changes to the class's source code.

Key Points:
- Decorator provides flexibility in adding or removing functionality dynamically.
- Subclassing adds behavior statically and affects all instances of the class.
- Decorators maintain the interface of the component they decorate, allowing them to be used interchangeably.

4. How would you implement multiple decorators for a single class?

Answer: Implementing multiple decorators for a single class involves "wrapping" the original class instance with multiple decorator classes, each adding its layer of functionality. Decorators can be nested within each other, allowing each decorator to add its functionality either before or after passing the call to the next wrapper in the chain.

Key Points:
- Multiple decorators can be applied to a single object by wrapping it with successive decorators.
- The order of decorators can affect the final outcome, depending on how they modify behavior.
- It's important to ensure that all decorators maintain the original interface to keep them interchangeable.

Example:

// Applying multiple decorators
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee); // First layer: Adding milk
coffee = new SugarDecorator(coffee); // Second layer: Adding sugar

Console.WriteLine($"{coffee.GetDescription()} Cost: {coffee.GetCost()}");

This code first wraps a SimpleCoffee object with a MilkDecorator, then wraps the result with a SugarDecorator, effectively applying both decorations to the original coffee object.