Composite Pattern
Overview
A file has a size; a folder must add the sizes of its files and subfolders. Testing for file or folder in every caller repeats the same branching logic.
Composite gives individual items and groups the same contract. A leaf has no children, while a composite holds other components.
UML
FileEntry and Folder derive from Entry. Folder refers to other Entry objects, allowing nested folders, and sums their Bytes values.
History
GoF cataloged Composite as a structural pattern in Design Patterns in 1994. It represents part-whole hierarchies through a shared interface.1
Why use it?
A caller reads Entry.Bytes in either case. Recursion inside Folder handles multiple levels of nesting.
Not every component needs a child-mutation operation. This example gives leaves a read operation and supplies a folder with its children at construction.
Example
Put two files in a folder and read the total. The example models entries in memory rather than accessing a filesystem.
using System;
using System.Collections.Generic;
using System.Linq;
Entry folder = new Folder(new FileEntry(10), new FileEntry(20));
Console.WriteLine(folder.Bytes);
// Give callers the same read contract for leaves and groups.
abstract record Entry
{
public abstract long Bytes { get; }
}
sealed record FileEntry(long Length) : Entry
{
public override long Bytes => Length;
}
// Read the shared property recursively instead of checking each child’s concrete type.
sealed record Folder : Entry
{
private readonly Entry[] children;
public Folder(params Entry[] children) => this.children = children.ToArray();
public override long Bytes => children.Sum(entry => entry.Bytes);
}
30Folder reads its children through Bytes without checking their concrete types.
An extended example
Nest a folder inside another folder, then read a single file and the group through the same Entry array.
using System;
using System.Collections.Generic;
using System.Linq;
Entry[] entries =
{
new FileEntry(7),
new Folder(new FileEntry(5), new Folder(new FileEntry(10), new FileEntry(20)))
};
foreach (Entry entry in entries) Console.WriteLine(entry.Bytes);
// Give callers the same read contract for leaves and groups.
abstract record Entry
{
public abstract long Bytes { get; }
}
sealed record FileEntry(long Length) : Entry
{
public override long Bytes => Length;
}
// Read the shared property recursively instead of checking each child’s concrete type.
sealed record Folder : Entry
{
private readonly Entry[] children;
public Folder(params Entry[] children) => this.children = children.ToArray();
public override long Bytes => children.Sum(entry => entry.Bytes);
}
7
35Folder copies its child array at construction and exposes no mutation API. A mutable version needs rules for preventing cycles and maintaining aggregate values and numeric limits.
TypeScript example
Entry lets callers read files and folders without distinguishing their concrete types. A file returns its own size; a folder sums its children, keeping recursive aggregation inside Folder.
// Give callers the same read contract for leaves and groups.
abstract class Entry { abstract get bytes(): bigint; }
class FileEntry extends Entry {
constructor(private readonly length: bigint) { super(); }
override get bytes(): bigint { return this.length; }
}
// Read the shared property recursively instead of checking each child’s concrete type.
class Folder extends Entry {
private readonly children: readonly Entry[];
constructor(children: readonly Entry[]) { super(); this.children = [...children]; }
override get bytes(): bigint {
return this.children.reduce((total, child) => total + child.bytes, 0n);
}
}
const entries: Entry[] = [
new FileEntry(7n),
new Folder([new FileEntry(5n), new Folder([new FileEntry(10n), new FileEntry(20n)])]),
];
for (const entry of entries) console.log(entry.bytes.toString());7
35Sizes use bigint to keep integer arithmetic, and are converted to strings for output. The constructor copies the child array; this example does not access a filesystem.
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Structural patterns, Composite. ↩