Decorator Design Pattern in Java

1. Definition

The Decorator Design Pattern dynamically adds or overrides behaviors in an object without changing its structure. It is a structural pattern that involves a set of decorator classes that are used to wrap concrete components. These decorators mirror the type of the components they decorate but add or override behavior.

2. Problem Statement

Imagine you have a simple coffee shop application where various beverages are sold. As new beverages or add-ons (like milk, sugar, whipped cream) are introduced, directly modifying the original classes can become complex and can violate the Open/Closed principle.

3. Solution

The Decorator Pattern allows behaviors to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class.

4. Real-World Use Cases

1. GUI toolkits where individual graphical components can be decorated with scroll bars, borders, shadows, etc.

2. Java I/O classes, like BufferedInputStream which adds buffering to another InputStream.

5. Implementation Steps

1. Define a component interface.

2. Implement the concrete component which needs to be decorated.

3. Create abstract decorator class referencing the component interface.

4. Implement concrete decorator classes extending the abstract decorator.

6. Implementation

// Step 1: Define the component interface
interface Beverage {
    String getDescription();
    double cost();
}

// Step 2: Implement concrete components
class Espresso implements Beverage {
    public String getDescription() {
        return "Espresso";
    }

    public double cost() {
        return 1.99;
    }
}

// Step 3: Create abstract decorator class
abstract class BeverageDecorator implements Beverage {
    protected Beverage beverage;
}

// Step 4: Implement concrete decorators
class Milk extends BeverageDecorator {
    public Milk(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Milk";
    }

    public double cost() {
        return beverage.cost() + 0.30;
    }
}

class Sugar extends BeverageDecorator {
    public Sugar(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Sugar";
    }

    public double cost() {
        return beverage.cost() + 0.10;
    }
}

// Step 5: Demonstrate the Decorator Pattern
public class DecoratorPatternDemo {
    public static void main(String[] args) {
        Beverage espresso = new Espresso();
        System.out.println(espresso.getDescription() + " $" + espresso.cost());

        Beverage espressoWithMilk = new Milk(espresso);
        System.out.println(espressoWithMilk.getDescription() + " $" + espressoWithMilk.cost());

        Beverage espressoWithMilkSugar = new Sugar(espressoWithMilk);
        System.out.println(espressoWithMilkSugar.getDescription() + " $" + espressoWithMilkSugar.cost());
    }
}

Output:

Espresso $1.99
Espresso, Milk $2.29
Espresso, Milk, Sugar $2.39

Explanation

In this demonstration, a basic espresso beverage is decorated first with milk and then with sugar. The decorators add their respective costs and descriptions to the base price and description of the espresso. The main advantage is the ease of extending or adding new functionalities without altering existing code.

7. When to use?

Use the Decorator Pattern when:

1. You need to add responsibilities to individual objects dynamically and transparently.

2. You want to avoid subclassing because it can result in an exponential explosion of subclasses.

3. You want to keep new functionality separate. For instance, adding enhancements.


Comments