The Decorator pattern is a structural design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. The decorator pattern is used when we want to add features to a class, but we don’t want to change the current implementation of the class directly.
The Decorator pattern works by extending an object’s behavior without changing its implementation. It’s usually implemented by creating a Decorator class which wraps the original object and provides additional functionality keeping the signature of the original interface.
A simple example to understand the decorator pattern would be a coffee shop, where customers can choose to add extra ingredients to their coffee, such as milk, sugar, whipped cream or caramel. These extra ingredients can be seen as decorators, where they enhance the original coffee, without changing the espresso itself. The Espresso class is wrapped with a MilkDecorator, SugarDecorator, WhippedCreamDecorator, or CaramelDecorator.
Here’s an example in Java:
// Component Interface
interface Coffee {
public double getCost(); // returns the cost of the coffee
public String getDescription(); // returns the description of the coffee
}
// Concrete Component
class Espresso implements Coffee {
@Override
public double getCost() {
return 1.99;
}
@Override
public String getDescription() {
return "Espresso";
}
}
// Decorator Class
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
protected String ingredientSeparator = ", ";
public CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}
@Override
public double getCost() { // delegated to concrete components
return decoratedCoffee.getCost();
}
@Override
public String getDescription() { // delegated to concrete components
return decoratedCoffee.getDescription();
}
}
// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public double getCost() {
return super.getCost() + 0.20;
}
@Override
public String getDescription() {
return super.getDescription() + ingredientSeparator + "Milk";
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public double getCost() {
return super.getCost() + 0.10;
}
@Override
public String getDescription() {
return super.getDescription() + ingredientSeparator + "Sugar";
}
}
In this example, we have a ‘Coffee‘ interface, which is implemented by the ‘Espresso‘ class. The ‘CoffeeDecorator‘ class is the abstract decorator and is inherited by ‘MilkDecorator‘ and ‘SugarDecorator‘. These decorators wrap the original ‘Espresso‘ object and add the ingredients (‘Milk‘ and ‘Sugar‘) to the coffee.
Here’s how we can use it:
Coffee coffee = new SugarDecorator(new MilkDecorator(new Espresso()));
System.out.println(coffee.getCost()); // Output: 2.29
System.out.println(coffee.getDescription()); // Output: Espresso, Milk, Sugar
In the above example, we can see that we are creating a new ‘Coffee‘ object and wrapping it with two decorators, ‘SugarDecorator‘ and ‘MilkDecorator‘, which adds to the cost of the coffee and the description.