Structural Design Pattern: Decorator

Reading Time: 4 minutes

In the field of software engineering, having codebases that are flexible and easily expandable is crucial. As software becomes more complex, the difficulty of incorporating new features and functionalities increases, all without making the code overly complex or disrupting its existing architecture. This article delves into the Decorator Design Pattern, a robust technique for improving object functionality while preserving code clarity and adaptability.

The Need for the Decorator Pattern:

Imagine a scenario where your application relies on a core object, providing essential functionalities. However, as the project progresses and stakeholder needs evolve, there arises a demand for additional features that cannot be predetermined or statically defined. This is where the Decorator Pattern comes into play. It caters to the necessity of dynamic feature enhancement by enabling developers to seamlessly incorporate new behaviors into objects without altering their fundamental structure.

One notable benefit of the Decorator Pattern is its ability to prevent the proliferation of subclasses that combine multiple behaviors. Instead of creating numerous subclasses to represent various combinations of features, the Decorator Pattern encourages a modular approach.

By utilizing multiple decorator classes, developers can compose objects with different combinations of behaviors dynamically. This approach promotes code reuse and maintains a clear separation of concerns. Each decorator encapsulates a single responsibility, facilitating easier management and modification of the codebase.

Common Implementation of the Decorator Pattern:

Implementing the Decorator Pattern typically involves a series of steps:

  1. Define Component Interface: Begin by creating an interface that declares the methods common to all concrete components. This interface serves as the blueprint for objects that will be decorated.
  2. Develop Concrete Component Class: Implement the interface in a concrete component class representing the base object with core functionalities.
  3. Design Decorator Base Class: Next, develop an abstract decorator class that implements the component interface. This base class holds a reference to the component interface to wrap other components.
  4. Create Concrete Decorator Classes: Implement concrete decorator classes that extend the decorator base class. Each decorator adds specific functionalities while delegating core functionalities to the wrapped component.
  5. Compose Decorators: Compose decorators by wrapping concrete components with one or more decorator classes. Decorators can be stacked, enabling flexible combinations of behaviors.
  6. Utilize Decorated Objects: Instantiate and use decorated objects as needed in the application. Invoking methods on decorated objects triggers the execution of both core and additional functionalities.

Example Implementation in C#:

To illustrate the Decorator Pattern in action, let’s consider a scenario where we have a basic car represented by the BasicCar class. We want to enhance this car with luxury features and sports features dynamically.

In this example, we start with a BasicCar and progressively decorate it with luxury features, sports features, and advanced technology features using concrete decorator classes. Each decorator adds its unique functionality while preserving the core behavior of the car.

// Component interface
interface ICar
{
    void Assemble();
}

// Concrete component
class BasicCar : ICar
{
    public void Assemble()
    {
        Console.WriteLine("Basic Car Assembled");
    }
}

// Decorator base class
abstract class CarDecorator(ICar decoratedCar) : ICar
{
    protected ICar DecoratedCar { get; } = decoratedCar;

    public virtual void Assemble()
    {
        DecoratedCar.Assemble();
    }
}

// Concrete decorator 1
class LuxuryCarDecorator(ICar decoratedCar) : CarDecorator(decoratedCar)
{
    public override void Assemble()
    {
        base.Assemble();
        Console.WriteLine("Adding luxury features");
    }
}

// Concrete decorator 2
class SportsCarDecorator(ICar decoratedCar) : CarDecorator(decoratedCar)
{
    public override void Assemble()
    {
        base.Assemble();
        Console.WriteLine("Adding sports features");
    }
}

// Concrete decorator 3
class AdvancedTechCarDecorator(ICar decoratedCar) : CarDecorator(decoratedCar)
{
    public override void Assemble()
    {
        base.Assemble();
        Console.WriteLine("Adding advanced technology features");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Creating a basic car
        ICar basicCar = new BasicCar();

        // Wrapping the basic car with multiple decorators
        LuxuryCarDecorator luxuryCar = new LuxuryCarDecorator(basicCar);
        SportsCarDecorator sportsLuxuryCar = new SportsCarDecorator(luxuryCar);
        AdvancedTechCarDecorator ultimateCar = new AdvancedTechCarDecorator(sportsLuxuryCar);

        // Assembling the ultimate car (basic car with additional features)
        ultimateCar.Assemble();
    }
}

Link to the GitHub repo: design-patterns/03-DecoratorPattern at master · antonespo/design-patterns (github.com)

The output presented in the console window is:

Basic Car Assembled
Adding luxury features
Adding sports features
Adding advanced technology features

Each decorator seamlessly integrates with the preceding one, enriching the car’s capabilities without cluttering its core implementation. The result is a modular and extensible design that allows for dynamic feature composition, ensuring that our luxury sports car remains adaptable to evolving requirements and user preferences.

Leave a Comment

Your email address will not be published. Required fields are marked *