Skip to main content

Composite

complexitypopularity

Arrange objects into a tree and give leaves and containers the same interface, so the client can treat one item and a whole branch identically.

The problem

You are building an ordering system with Product and Box. A box holds products, and also smaller boxes, which hold products, and possibly still smaller boxes. Someone asks for the total price of an order.

In the physical world you would simply tear everything open and add up the labels. In code you would have to know the classes involved, the nesting depth, and which type check to run at each level, before writing the loop. Then you would write it again, from scratch, for total weight. And again for the packing slip.

The solution

Give products and boxes one shared interface with a getPrice() method. A product returns its price. A box asks each of its contents for a price, adds them up, maybe adds packaging cost, and returns the total. If one of those contents is another box, it does the same thing one level deeper, and so on until the recursion runs out of tree.

The payoff is that the client stops caring about concrete classes entirely. Call the method on the top node and the objects themselves route the request downward.

ImageEditorCompoundGraphic (all)CompoundGraphic (group)Circle (leaf)add(new Dot(1, 2))1add(group)2draw()3draw()4draw()5bounds6merged bounds7done8
  1. 1The client fills the tree through the component interface. A dot and a group are the same kind of argument.
  2. 2A container gets added to a container. Nothing special happens, which is exactly the point.
  3. 3One call on one object. The client has no idea how deep this goes.
  4. 4The root loops over its children and forwards the request, never asking what class anything is.
  5. 5The group does the same thing one level down. Recursion is the implementation and the point.
  6. 6The leaf does the real drawing and reports its bounding box back up.
  7. 7Each container folds its children’s results into one and passes the summary onward.
  8. 8The whole tree rendered from a single call, with the client coupled to nothing but Graphic.

Structure

Four roles: the component interface shared by everything, leaves with no children that do the real work, containers that hold components and delegate, and the client that talks to the whole tree as if it were one object.

«interface»Graphicmove(x, y)draw()COMPONENTDotx, ymove(x, y)draw()LEAFCircleradiusdraw()LEAFCompoundGraphicchildren: Graphic[]add(child)remove(child)move(x, y)draw()CONTAINERImageEditorall: CompoundGraphicload()groupSelected(components)CLIENT
implementsextendsuses

Code

A graphics editor where a dot, a circle and a group of a hundred shapes are all just a Graphic.

// No shared interface, so the client does the recursion by hand
// and must know every class in the tree.
method drawEverything(items) is
foreach (item in items) do
if (item is Dot) then
item.drawDot()
else if (item is Circle) then
item.drawCircle()
else if (item is CompoundGraphic) then
// Recurse manually, and remember to merge the bounds...
drawEverything(item.getChildren())
else
throw new Error("what even is this")
 
// Now write totalPrice(), move(), select() and export() the same way,
// and update all five every time a new shape ships.
// Leaves and containers answer to the same interface.
interface Graphic is
method move(x, y)
method draw()
 
class CompoundGraphic implements Graphic is
field children: array of Graphic
 
method draw() is
foreach (child in children) do
child.draw() // leaf or container, nobody checks
 
// The client's entire drawing code:
all.draw()
 
// Grouping five shapes is now a tree edit, not a special case:
group = new CompoundGraphic()
group.add(selectedShapes)
all.add(group)
// The component interface declares operations that make sense for
// both a single shape and a pile of them.
interface Graphic is
method move(x, y)
method draw()
 
// A leaf sits at the end of the tree and has no children, which is
// why leaves do most of the honest work.
class Dot implements Graphic is
field x, y
 
constructor Dot(x, y) { ... }
 
method move(x, y) is
this.x += x, this.y += y
 
method draw() is
// Paint a dot at X and Y.
 
// Leaves are allowed their own hierarchy.
class Circle extends Dot is
field radius
 
constructor Circle(x, y, radius) { ... }
 
method draw() is
// Paint a circle at X and Y with radius R.
 
// The container holds components - leaves, other containers, it
// cannot tell and does not want to.
class CompoundGraphic implements Graphic is
field children: array of Graphic
 
method add(child: Graphic) is
// Append to the child list.
 
method remove(child: Graphic) is
// Drop from the child list.
 
method move(x, y) is
foreach (child in children) do
child.move(x, y)
 
method draw() is
// 1. For each child: draw it and grow the bounding box.
// 2. Paint a dashed rectangle around the combined bounds.
 
// The client only ever names the component interface.
class ImageEditor is
field all: CompoundGraphic
 
method load() is
all = new CompoundGraphic()
all.add(new Dot(1, 2))
all.add(new Circle(5, 3, 10))
 
// Grouping is just moving nodes around in the tree.
method groupSelected(components: array of Graphic) is
group = new CompoundGraphic()
foreach (component in components) do
group.add(component)
all.remove(component)
all.add(group)
// One call redraws the whole thing, group and all.
all.draw()

When to use it

  • The model is a tree: file systems, org charts, UI widget hierarchies, nested orders, scene graphs, expression trees.
  • You want client code to treat a single element and a whole subtree the same way, so that adding a new node type does not send you editing every consumer.

Pitfalls

  • The overgeneralized component. If leaves and containers differ too much, the shared interface turns into a lowest common denominator that describes nothing well.
  • Child management on the component interface. add() on a leaf is a lie you will have to implement somehow. Choose uniformity or honesty and document which.
  • Cycles. Nothing in the pattern stops you adding a container to its own subtree. The first symptom is a stack overflow on the next draw().
  • Silent cost. A single innocent-looking call may touch tens of thousands of nodes. Caching aggregate results is a common follow-up, and invalidating them is the usual next bug.

Don't confuse it with

  • Decorator. Structurally a decorator is a composite with exactly one child. The difference is motive: a decorator adds responsibilities on the way through, while a composite sums up what its children report. They cooperate happily - decorate one node inside a composite tree and nobody else notices.
  • Chain of Responsibility. CoR passes a request along until somebody handles it, then stops. Composite fans out to everyone and aggregates. Combined, they are how a leaf bubbles an event up to the root.
  • Iterator. An iterator traverses a structure from outside; Composite is the structure, traversing itself from inside. Use an Iterator to walk a Composite tree, or a Visitor to run an operation over all of it.
  • Flyweight. Not a rival: shared leaf nodes in a big Composite tree are prime candidates to be implemented as flyweights when memory gets tight.

Check yourself

Question 1 of 5

What is the precondition for reaching for Composite?