Composite and Decorator Patterns
The Composite Pattern
Intent: Treat single objects and groups of objects uniformly through one shared interface. The client does not need to know whether it is dealing with a leaf (individual object) or a composite (group).
The problem
Imagine a DVD shop that sells individual films and box sets. You want a single price() method that works on both — without duplicating the pricing logic or writing if (item instanceof BoxSet) checks everywhere.
The solution
Define an interface that both the individual and the group implement:
interface PriceableItem {
double price();
}
The individual (leaf) implements it directly:
class Film implements PriceableItem {
private String title;
private double price;
Film(String title, double price) {
this.title = title;
this.price = price;
}
public double price() {
return price;
}
}
The group (composite) also implements it, delegating to its children:
class BoxSet implements PriceableItem {
private List<PriceableItem> items = new ArrayList<>();
void add(PriceableItem item) {
items.add(item);
}
public double price() {
return items.stream()
.mapToDouble(PriceableItem::price)
.sum() * 0.9;
}
}
Why this works
Client code calls .price() identically whether it holds a Film or a BoxSet. The composite’s price() internally iterates over its children and aggregates — the recursion handles arbitrarily deep nesting (a box set containing another box set containing films).
It satisfies the Open-Closed Principle: new item types (e.g., RentalFilm) can be added by implementing PriceableItem, without touching any existing code.
Composite structure
The Decorator Pattern
Intent: Attach additional responsibilities to an individual object dynamically at runtime, without subclassing every possible combination.
The problem
Imagine a bookshop that sells plain books, signed copies, and gift-wrapped copies. You could subclass: GiftWrappedBook, SignedBook, GiftWrappedSignedBook, SignedGiftWrappedBook, and so on — combinatorial explosion. Each new feature doubles the number of subclasses you might need.
The solution
Define a common interface, a plain implementation, and wrapper classes that each add one behaviour:
interface Book {
double cost();
}
class PlainBook implements Book {
private double baseCost;
PlainBook(double baseCost) {
this.baseCost = baseCost;
}
public double cost() {
return baseCost;
}
}
class GiftWrapped implements Book {
private final Book inner;
GiftWrapped(Book inner) {
this.inner = inner;
}
public double cost() {
return inner.cost() + 2.0;
}
}
class SignedCopy implements Book {
private final Book inner;
SignedCopy(Book inner) {
this.inner = inner;
}
public double cost() {
return inner.cost() + 5.0;
}
}
Decorators can be stacked arbitrarily:
Book myBook = new GiftWrapped(new SignedCopy(new PlainBook(10.0)));
System.out.println(myBook.cost()); // 10.0 + 5.0 + 2.0 = 17.0
Why this works
Each decorator wraps another Book instance (the “inner”), delegates the core work to it, and adds its own behaviour. The decorator is-a Book (implements the interface) and has-a Book (holds the inner reference). This is sometimes called the “is-a and has-a” structure.
Key differences: Composite vs Decorator
| Aspect | Composite | Decorator |
|---|---|---|
| Purpose | Treat one and group uniformly | Add behaviour to one object dynamically |
| Structure | Tree (parent → many children) | Chain (one wrapper → one inner) |
| Client intent | Client does not care if leaf or group | Client explicitly wraps to add behaviour |
| Aggregation | One-to-many (a BoxSet holds a List) | One-to-one (a GiftWrapped holds one Book) |
| Recursion | Composite.price() calls child.price() on many | Decorator.cost() calls inner.cost() on one |
| Example | GUI: a Button and a Panel containing Buttons both have a render() method | I/O: BufferedReader wraps a Reader to add buffering |
Both satisfy OCP
- Composite: Add a new leaf type by implementing the interface. Existing composites and client code are unchanged.
- Decorator: Add a new decoration by writing a new wrapper class. Existing wrappers and plain implementations are unchanged.
Tripos traps
-
Conflating the two — A question may describe a scenario and ask you to name the pattern. If it is about groups and individuals acting uniformly, it is Composite. If it is about wrapping one object to add behaviour, it is Decorator. Argue from intent, not from structure alone.
-
Forgetting the interface — Both patterns depend on a shared interface that both the leaf/base and the composite/decorator implement. Without the interface, neither pattern works.
-
Decorator without delegation — A decorator must delegate to the inner object. If it replaces behaviour entirely rather than augmenting it, it is not a true Decorator.