The Decorator pattern provides a flexible alternative to inheritance for extending the functionality of a class at runtime. However, when we have multiple decorators in use, we may need to apply them in a specific order to achieve the desired behavior.
To handle multiple decorators in a specific order, we can chain them together in a particular sequence. Each decorator wraps the component it decorates, and the wrapped component is passed along the chain until it reaches the final decorator in the sequence. In this way, each decorator adds its own behavior to the component and passes the modified component to the next decorator in the chain until the final behavior is achieved.
To illustrate this, let’s consider an example of a coffee shop application that allows customers to order different types of coffee with extra toppings. We can have multiple decorators that add different toppings, such as milk, sugar, whipped cream, and cinnamon.
We can start by defining an interface representing the basic coffee component:
public interface Coffee {
double getCost();
String getDescription();
}Next, we can create a concrete implementation of the coffee component:
public class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 1.0;
}
@Override
public String getDescription() {
return "Simple coffee";
}
}Now, we can define the decorator interface, which extends the Coffee interface and adds the functionality of adding extra toppings:
public interface CoffeeDecorator extends Coffee {
}We can implement the decorator interface for each topping we want to add, such as milk, sugar, whipped cream, and cinnamon. Each decorator adds the cost and description of its topping to the wrapped coffee component:
public class MilkDecorator implements CoffeeDecorator {
private Coffee coffee;
public MilkDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost() + 0.5;
}
@Override
public String getDescription() {
return coffee.getDescription() + ", milk";
}
}
public class SugarDecorator implements CoffeeDecorator {
private Coffee coffee;
public SugarDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost() + 0.2;
}
@Override
public String getDescription() {
return coffee.getDescription() + ", sugar";
}
}
// Similar decorators for whipped cream and cinnamonFinally, we can use the decorators in a specific order to create a custom coffee with the desired toppings:
Coffee customCoffee = new SugarDecorator(new WhippedCreamDecorator(
new MilkDecorator(new SimpleCoffee())));
System.out.println(customCoffee.getDescription() + " costs $" + customCoffee.getCost());In this example, we apply the decorators in the order of Milk, WhippedCream, Sugar, and SimpleCoffee. The output of the code will be:
Simple coffee, milk, whipped cream, sugar costs $2.2In case of potential conflicts, we need to carefully design our decorators to avoid overriding each other’s functionality. For example, we can have a decorator that adds more coffee instead of water, and this decorator should be applied before any decorator that adds milk, to avoid diluting the coffee flavor. In general, we should follow the Single Responsibility Principle and design our decorators to add independent and cohesive features to the component they decorate.