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);
abstract record Entry
{
public abstract long Bytes { get; }
}
sealed record FileEntry(long Length) : Entry
{
public override long Bytes => Length;
}
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);
abstract record Entry
{
public abstract long Bytes { get; }
}
sealed record FileEntry(long Length) : Entry
{
public override long Bytes => Length;
}
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.
Related reading
Each example is a separate .NET 10 console program.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Structural patterns, Composite. ↩