Composite Pattern for Tree Hierarchies in C
The Composite pattern is a structural design pattern that allows you to compose objects into tree-like structures to represent part-whole hierarchies. This article explains the core mechanics of the Composite pattern in C#, breaks down its key components—Component, Leaf, and Composite—and provides a clear, practical code implementation demonstrating how to treat individual objects and complex compositions uniformly.
Core Architecture of the Pattern
The Composite pattern is built on three fundamental elements:
- Component: An interface or abstract class defining operations common to both simple and complex elements of the tree.
- Leaf: The basic building block representing end nodes that have no children. A leaf performs the actual work for its operations.
- Composite: A container node that stores child components (both leaves and other composites). It implements the component interface, usually by delegating operations recursively to its children.
C# Implementation: File System Hierarchy
A file system is a classic example of a tree hierarchy: files are leaf nodes, and directories are composites that can contain both files and other directories.
using System;
using System.Collections.Generic;
// 1. Component
public interface IFileSystemItem
{
string Name { get; }
long GetSizeInBytes();
void Display(int depth = 0);
}
// 2. Leaf
public class FileItem : IFileSystemItem
{
public string Name { get; }
private readonly long _sizeInBytes;
public FileItem(string name, long sizeInBytes)
{
Name = name;
_sizeInBytes = sizeInBytes;
}
public long GetSizeInBytes() => _sizeInBytes;
public void Display(int depth = 0)
{
Console.WriteLine($"{new string('-', depth * 2)} File: {Name} ({_sizeInBytes} bytes)");
}
}
// 3. Composite
public class DirectoryItem : IFileSystemItem
{
private readonly List<IFileSystemItem> _children = new();
public string Name { get; }
public DirectoryItem(string name)
{
Name = name;
}
public void Add(IFileSystemItem item) => _children.Add(item);
public void Remove(IFileSystemItem item) => _children.Remove(item);
public long GetSizeInBytes()
{
long total = 0;
foreach (var child in _children)
{
total += child.GetSizeInBytes();
}
return total;
}
public void Display(int depth = 0)
{
Console.WriteLine($"{new string('-', depth * 2)}+ Directory: {Name}");
foreach (var child in _children)
{
child.Display(depth + 1);
}
}
}Usage Example
Client code interacts with the tree hierarchy entirely through the
IFileSystemItem interface, eliminating the need to check
whether an item is a file or a folder.
class Program
{
static void Main()
{
var root = new DirectoryItem("Root");
var docs = new DirectoryItem("Documents");
var images = new DirectoryItem("Images");
docs.Add(new FileItem("resume.pdf", 250000));
docs.Add(new FileItem("notes.txt", 15000));
images.Add(new FileItem("photo.png", 1200000));
root.Add(docs);
root.Add(images);
root.Add(new FileItem("config.json", 1200));
// Treats the entire tree uniformly
root.Display();
Console.WriteLine($"Total Size: {root.GetSizeInBytes()} bytes");
}
}When to Use the Composite Pattern
- Hierarchical Representations: Whenever your application needs to represent tree-like nested structures, such as UI control trees, organization charts, or product catalogs.
- Uniform Client Code: When you want clients to
ignore the difference between collections of objects and individual
objects, simplifying logic and reducing conditional branching
(
if/elsechecks for types).
Benefits and Trade-offs
- Simplifies the Client: Clients do not need to maintain complex state or recursive traversal logic. Calling a method on the root node processes the entire hierarchy.
- Open/Closed Principle: You can introduce new leaf or composite types into the tree without breaking existing client code.
- Overly General Designs: Enforcing constraints—such as ensuring a certain folder type can only contain specific file types—can be difficult with a purely uniform interface, requiring runtime checks rather than compile-time type safety.