Introduction

The Decorator pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. This pattern is particularly useful for adhering to the Single Responsibility Principle by allowing functionality to be divided between classes with unique areas of concern.

Key Concepts

  • Component: The interface or abstract class defining the methods that will be implemented.
  • Concrete Component: The class that implements the Component interface.
  • Decorator: An abstract class that implements the Component interface and contains a reference to a Component object.
  • Concrete Decorators: Classes that extend the Decorator class and add functionalities to the Component.

Structure

The Decorator pattern can be visualized with the following UML diagram:

Component
  + operation()
    |
ConcreteComponent
  + operation()
    |
Decorator
  - component: Component
  + operation()
    |
ConcreteDecoratorA
  - addedState: Type
  + operation()
  + addedBehavior()
    |
ConcreteDecoratorB
  - addedState: Type
  + operation()
  + addedBehavior()

Example

Let's consider a simple example where we have a Coffee interface and we want to add different types of condiments to the coffee dynamically.

Step 1: Define the Component Interface

public interface Coffee {
    double cost();
    String description();
}

Step 2: Create a Concrete Component

public class SimpleCoffee implements Coffee {
    @Override
    public double cost() {
        return 5.0;
    }

    @Override
    public String description() {
        return "Simple Coffee";
    }
}

Step 3: Create the Decorator Abstract Class

public abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    @Override
    public double cost() {
        return decoratedCoffee.cost();
    }

    @Override
    public String description() {
        return decoratedCoffee.description();
    }
}

Step 4: Create Concrete Decorators

public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double cost() {
        return super.cost() + 1.5;
    }

    @Override
    public String description() {
        return super.description() + ", Milk";
    }
}

public class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double cost() {
        return super.cost() + 0.5;
    }

    @Override
    public String description() {
        return super.description() + ", Sugar";
    }
}

Step 5: Use the Decorators

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        System.out.println(coffee.description() + " $" + coffee.cost());

        coffee = new MilkDecorator(coffee);
        System.out.println(coffee.description() + " $" + coffee.cost());

        coffee = new SugarDecorator(coffee);
        System.out.println(coffee.description() + " $" + coffee.cost());
    }
}

Output

Simple Coffee $5.0
Simple Coffee, Milk $6.5
Simple Coffee, Milk, Sugar $7.0

Practical Exercises

Exercise 1: Implement a Decorator

Task: Implement a WhippedCreamDecorator that adds whipped cream to the coffee.

Solution:

public class WhippedCreamDecorator extends CoffeeDecorator {
    public WhippedCreamDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double cost() {
        return super.cost() + 2.0;
    }

    @Override
    public String description() {
        return super.description() + ", Whipped Cream";
    }
}

Exercise 2: Chain Multiple Decorators

Task: Create a SimpleCoffee and add Milk, Sugar, and WhippedCream to it. Print the final description and cost.

Solution:

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee);
        coffee = new SugarDecorator(coffee);
        coffee = new WhippedCreamDecorator(coffee);

        System.out.println(coffee.description() + " $" + coffee.cost());
    }
}

Output

Simple Coffee, Milk, Sugar, Whipped Cream $9.0

Common Mistakes

  • Not using the Decorator pattern correctly: Ensure that the decorators are extending the abstract decorator class and not the concrete component directly.
  • Forgetting to call the super methods: When overriding methods in the decorator, always call the corresponding method of the decorated component to maintain the chain of responsibility.

Conclusion

The Decorator pattern is a powerful tool for extending the functionality of objects dynamically. It promotes code reusability and adheres to the Single Responsibility Principle by allowing functionalities to be divided among different classes. By practicing the exercises and understanding the examples, you should now have a solid grasp of how to implement and use the Decorator pattern in your projects.

© Copyright 2024. All rights reserved