Home Design Patterns Composite
Composite
Structural Gang of Four“Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.”
Gamma, Helm, Johnson & Vlissides, p. 163Purpose
Composites help when dealing with structures that are made up of other structures. For example, a graphics object might be made of Lines, Rectangles, Text, and other graphic objects. A Composite groups all of these objects together so they can be manipulated as a group.
Design
In order to treat objects and compositions of objects uniformly, a Composite pattern uses an abstract class that represents both primitives and their containers. This simple idea allows a developer to control a large number of items as if they were only one item, or to control an individual item as needed.
The decision that carries the most weight is where to put the child-management
methods, add() and remove(). Declaring them on
the Component gives you perfect uniformity, at the cost of a Leaf that has to
implement operations which make no sense for it. Declaring them only on the
Composite keeps the types honest, but now the client has to know which kind it is
holding, which is the exact thing the pattern set out to avoid. The Gang of Four
lean toward uniformity; I lean the other way, but there is no free answer here.
Example
A typical customer of Autobot Robotics uses the robotic systems in a production line with one product or another, acting sequentially or simultaneously. The task of organizing these production tasks can be daunting.
To simplify the problem, the designers created an abstraction of the tasks so each task either stands alone or is made up of other tasks. This allows the designers to refer to large groups of tasks that work together as a single unit and manipulate them as a single unit, instead of having to work with dozens of tasks each individually.
class Task
{
public void doTask() { }
public void taskSteps() { }
public void addTask(Task t) { }
public void removeTask(Task t) { }
public Task[] getChildren() { return new Task[0]; }
}
class AssemblyLine : Task
{
public void doTask() {
taskSteps();
Task[] childTasks = getChildren();
for (int i = 0; i < childTasks.Count(); i++) {
childTasks[i].doTask();
}
}
public void taskSteps() { }
public void addTask(Task t) { }
public void removeTask(Task t) { }
public Task[] getChildren() { return new Task[5]; }
}
class BeltTask : Task {
public void taskSteps() {
// specific belt tasks
}
}
class ArmTask : Task {
public void taskSteps() {
// specific arm tasks
}
}
AssemblyLine.doTask() is the
recursive case: it runs its own steps, then calls doTask() on
each child without caring whether that child is a leaf or another
line.Comparisons
- Decorator has almost the same UML. Both are a class that is a Component and holds a Component. A Decorator holds exactly one and adds behavior; a Composite holds many and adds structure.
- Iterator is how you walk the finished tree without exposing its shape.
- Visitor is how you add operations across a Composite without editing every node class.
- Flyweight combines well with Composite when the tree gets large: shared leaves let one node object appear in many places.
Why It Exists
My take
Composite exists because hierarchy is everywhere in the problems we model. Assemblies of parts, folders of files, groups of tasks, expressions built from sub-expressions. It also exists because the naive code for handling hierarchy is uniquely bad. Without the pattern, every operation that walks the structure grows a type test at the top: if this is a single thing do X, if it is a group, loop and recurse. That conditional then gets copy-pasted into every function that touches the tree, and each copy is a chance to forget the recursive case.
What the pattern really does is push the recursion into the type system, so it happens once instead of at every call site. The client stops asking "which kind is this?" because the answer stopped mattering.
The Autobot task grouping is the version of this I have the most confidence in, because it survived contact with real operators. People naturally think in groups. They say "run the whole board sequence", not "run these eleven steps", and a Composite lets the software use the same nouns the person on the floor already uses. That alignment matters more than the class count.
Criticisms
It can make the design overly general. Sometimes it is important to differentiate between the different paths and layers of the tree, and a Composite actively works to hide exactly that. It also makes it hard to constrain what a container may hold: if everything is a Component, the type system will happily let you put a drill task inside a cleanup task, and you are back to enforcing the rules at runtime.