Commissioned for this museum · after Gamma, Helm, Johnson & Vlissides, 1994
Composite
TypeScript·1994·43 lines·910 bytes
/**
* Composite — a leaf and a branch answering the same question, so that
* callers stop having to ask which one they are holding.
*/
type Node = File | Folder;
class File {
constructor(
readonly name: string,
private readonly bytes: number,
) {}
size(): number {
return this.bytes;
}
}
class Folder {
private readonly children: Node[] = [];
constructor(readonly name: string) {}
add(child: Node): this {
this.children.push(child);
return this;
}
// The recursion lives here, once, instead of in every caller that ever
// wants to know how big something is.
size(): number {
return this.children.reduce((total, child) => total + child.size(), 0);
}
}
const tree = new Folder("museum")
.add(new File("index.html", 1200))
.add(
new Folder("halls")
.add(new File("patterns.html", 3400))
.add(new File("commons.html", 2600)),
);
console.log(tree.size() + " bytes");Curator’s note
The whole pattern is that size() appears twice with the same signature and means the same thing. Once it does, a caller holding a Node can ask how big it is without knowing whether the answer will cost one property read or a walk of ten thousand descendants. The tree stops being something callers traverse and becomes something they interrogate.
The 1994 version had a problem this one does not, and it is worth knowing because the fix is a language feature rather than a better drawing. In the book, Component declares add so that callers can treat everything uniformly, which forces Leaf to implement an add that cannot work — the standard answer being to throw at runtime. Here Node is a union, File simply has no add, and the compiler stops the mistake before the program runs. Uniform where uniformity is true, distinct where it is not.
What you pay is that the cost of an operation is now hidden behind an identical-looking call. file.size() is free and folder.size() may not be, and nothing at the call site distinguishes them. Every performance problem people have with composites is a version of this: something innocent-looking inside a loop, quietly walking a subtree each time round. The pattern is doing exactly what it promised; the promise is just more expensive than it reads.
The reason it turns up everywhere — file systems, scene graphs, the DOM, every UI framework's element tree — is that recursion in the data is easier to hold in your head than recursion in the code that visits it.