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:

  1. Component: An interface or abstract class defining operations common to both simple and complex elements of the tree.
  2. Leaf: The basic building block representing end nodes that have no children. A leaf performs the actual work for its operations.
  3. 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/else checks 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.