The composite pattern is a structural design pattern that allows you to group objects into tree-like structures, enabling you to treat individual objects and groups of objects uniformly.
This pattern is based on recursive composition, allowing you to build complex structures using simple components. All components share a common interface, making it possible to interact with single objects and groups of objects in the same way.
Applicability
- When you need to represent hierarchies of objects (e.g., arithmetic expressions, file systems, organisational charts).
- When clients should treat individual objects and compositions uniformly, without knowing whether they are dealing with a single object or a whole group.
Pros and cons
Advantages
- Uniformity and simplification
- Clients use the same interface for all objects in the hierarchy, simplifying the interaction logic.
- Enables recursive tree structures where operations apply consistently at every level.
- Easier extension of new component types
- New types of components (leaf or composite) can be added without affecting existing code.
- Supports recursive composition
- Allows composites to contain other composites, enabling arbitrary depth of nesting.
- Encapsulates the client from the complex structure of objects
- The client doesn’t need to understand how many layers of objects exist. It just calls the top-level operation and the composite object handles everything internally.
Drawbacks
- Can lead to a large number of classes in the hierarchy
- Each element in the structure (leaf types, composite types) typically requires its own class, and as the system grows in complexity (e.g. more operations like Add, Subtract, Divide), the number of classes can increase significantly.
- Harder to restrict certain behaviours
- Leaf and composite nodes share the same interface; thus, you cannot enforce that certain operations are only applicable to composites (e.g., adding children. you can’t let leafs to add children).
Approach
- Define a component interface that declares operations applicable to both simple (leaf) and complex (composite) objects.
- Implement leaf classes that represent primitive elements of the structure.
- Implement composite classes that can contain child components (leafs or other composites) and implement the same interface.
- Client code uses the component interface to interact with all objects uniformly, regardless of whether they are leaf or composite.
classDiagram class Component { <<abstract>> +operation() } class Leaf { +operation() } class Composite { -children: List[Component] +add(component: Component) +remove(component: Component) +operation() } Component <|-- Leaf Component <|-- Composite Composite --> Component : contains
Components
- Component (abstract)
- Declares the interface for objects in the composition. Defines common operations (e.g.,
evaluate()
in arithmetic expressions).
- Declares the interface for objects in the composition. Defines common operations (e.g.,
- Leaf
- Represents the basic, indivisible objects (e.g., numbers or variables).
- Composite
- Maintains a collection of child components and implements child-related operations like
add()
or recursiveevaluate()
.
- Maintains a collection of child components and implements child-related operations like
Example
Use the Composite Pattern to allow individual products and groups of products (boxes) to be treated uniformly when calculating total price.
Components
public interface Item {
double getPrice();
// This method only makes sense for composites, not for leaves (products)
default void add(Item item) {
throw new UnsupportedOperationException("Cannot add item to a leaf");
}
}
Leaf
public class Product implements Item {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public double getPrice() {
return price;
}
@Override
public String toString() {
return name + " ($" + price + ")";
}
}
Composite
import java.util.ArrayList;
import java.util.List;
public class Box implements Item {
private List<Item> items = new ArrayList<>();
@Override
public double getPrice() {
return items.stream()
.mapToDouble(Item::getPrice)
.sum();
}
@Override
public void add(Item item) {
items.add(item);
}
@Override
public String toString() {
return "Box (Total: $" + getPrice() + ")";
}
}
Client
public class CompositeDemo {
public static void main(String[] args) {
Product p1 = new Product("Book", 12.99);
Product p2 = new Product("Pen", 1.99);
Product p3 = new Product("Headphones", 89.50);
Box box1 = new Box();
box1.add(p1);
box1.add(p2);
Box box2 = new Box();
box2.add(box1); // add another box inside
box2.add(p3);
System.out.println("Total price of box2: $" + box2.getPrice());
}
}
Back to parent page: Structural Patterns
Design_Pattern Structural_Design_Patterns Composite_Pattern SOFT3202