Decorator Pattern
Overview
Decorator pattern is a structural patttern that lets you attach new behaviours to objects by placing these objects inside special wrapper objects that contain the behaviours. Multiple decorators can be added to a single object and the behaviours stacked recursively.
Advantages
- Allows for extension of an object’s behaviour without making a new subclass.
- Can add or remove responsibilities from an object at runtime.
- Can combine several behaviours by wrapping an object into multiple decorators.
- Satisfies single responsibility principle by dividing a monolithic class into several smaller decorating classes.
- Satisfies open/closed principle by not requiring changes in the original class when adding new functionality.
Disadvantages
- Hard to remove specific wrappers from the wrappers stack.
- Decorating order often matters, introducing complexity with ordering.
Applicability
- When you need to be able to assign extra behaviours to objects at runtime without breaking the code that uses these objects.
- When it’s awkward or not possible to extend an object’s behaviour using inheritance.
- When you have multiple optional behaviours that you want to be able to dynamically add to objects.
Example
public interface Coffee {
public double getCost();
public String getIngredients();
}
public class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 1;
}
@Override
public String getIngredients() {
return "Coffee";
}
}
public abstract class CoffeeDecorator implements Coffee {
private final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
@Override
public String getIngredients() {
return decoratedCoffee.getIngredients();
}
}
public class WithMilk extends CoffeeDecorator {
public WithMilk(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return super.getCost() + 0.5;
}
@Override
public String getIngredients() {
return super.getIngredients() + ", Milk";
}
}
public class WithSprinkles extends CoffeeDecorator {
public WithSprinkles(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return super.getCost() + 0.2;
}
@Override
public String getIngredients() {
return super.getIngredients() + ", Sprinkles";
}
}
public class Main {
public static void printInfo(Coffee coffee) {
System.out.println("Cost: " + coffee.getCost())
+ "; Ingredients: " + coffee.getIngredients());
}
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
printInfo(coffee);
// Decorate coffee once.
coffee = new WithMilk(coffee);
printInfo(coffee);
// Decorate coffee again.
// Tracing path of cost calls:
// coffee.getCost()
// -> WithSprinkles.getCost()
// -> CoffeeDecorator.getCost()
// -> WithMilk.getCost()
// -> CoffeeDecorator.getCost()
// -> SimpleCoffee.getCost()
// Essentially:
// Alternate returns to the base CoffeeDecorator
// each time but with one less decorating layer.
coffee = new WithSprinkles(coffee);
printInfo(coffee);
// Output:
// Cost: 1.0; Ingredients: Coffee
// Cost: 1.5; Ingredients: Coffee, Milk
// Cost: 1.7; Ingredients: Coffee, Milk, Sprinkles
}
}