Composite
To find a folder's size you write loops that check "is this a file or a folder?" at every level.
Give leaves and groups the same interface (total()).
Your data is a tree: part and whole
The picture from life
On your laptop a folder can contain files and other folders. When you ask "how big is this?", the answer works the same whether you point at one file or at a folder with a hundred nested things. A folder just asks each child for its size and adds them up.
The problem
What goes wrong when you do not use it.
To find a folder's size you write loops that check "is this a file or a folder?" at every level. Each new kind of operation (count, search, delete) repeats the same type checks.
- calls / uses
- Your data is a tree: part and whole
- Callers should not care whether they hold a leaf or a group
- The same operation should run on the whole tree
The idea
How the pattern fixes it.
Give leaves and groups the same interface (total()). A leaf answers itself. A group answers by asking its children and combining. The caller just calls total() and never checks types.
- implements
Who is who
- Node (interface)
- Common promise: total().
- File (leaf)
- Returns its own size.
- Folder (composite)
- Holds children and sums their total().
Build it in steps
- 1Define one interface for leaf and group.
- 2Implement it in the leaf directly.
- 3Implement it in the group by looping over children.
- 4Call it from the top of the tree.
Watch it run
Step through the calls. Tap an arrow to jump to it.
The client asks the root folder for its size. It does not care that it is a folder.
total()The code
Python and Java. Try writing the classes yourself from the diagram first.
class File:
def __init__(self, name, kb):
self.name = name
self.kb = kb
def total(self):
return self.kb
class Folder:
def __init__(self, name, children=None):
self.name = name
self.children = children or []
def total(self): # same call as a File, answered by the children
return sum(child.total() for child in self.children)
def demo():
docs = Folder("docs", [File("b.txt", 20), File("c.txt", 30)])
root = Folder("root", [File("a.txt", 10), docs, File("d.txt", 115)])
return root.total(), docs.total(), File("x", 7).total()What you gain
- One call works on a leaf or a whole branch
- Adding a new kind of node needs no changes to callers
What it costs
- The interface can end up too general (a File gets add() it cannot use)
- Hard to restrict what a group may hold
You have already met it in
- The HTML/DOM tree and UI widget trees (views holding views)
- File systems and org charts
- Expression trees in compilers
Often mixed up with
Make it stick
Model a company: Employee (leaf) and Manager (group of employees and managers). Compute total salary of any manager's whole team with one call.