Composite Pattern: Hierarchical Structural Modeling

The Composite pattern treats individual objects and compositions of objects uniformly. It is the architectural foundation for tree structures, allowing clients to ignore the difference between a "Leaf" (terminal node) and a "Composite" (container node).

1. Concrete Implementation: File System Explorer

In a file system, both File and Directory share a common interface for operations like getSize() or list().

1.1 The Component Interface

import java.util.List;

public interface FileSystemNode {
    String getName();
    long getSize();
    void print(String indent);
}

1.2 The Leaf (File)

public record File(String name, long size) implements FileSystemNode {
    @Override
    public String getName() { return name; }
    
    @Override
    public long getSize() { return size; }

    @Override
    public void print(String indent) {
        System.out.println(indent + "📄 " + name + " (" + size + " bytes)");
    }
}

1.3 The Composite (Directory)

The Composite node manages a collection of components and recursively delegates operations.

import java.util.ArrayList;
import java.util.List;

public class Directory implements FileSystemNode {
    private final String name;
    private final List<FileSystemNode> children = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    public void add(FileSystemNode node) {
        children.add(node);
    }

    @Override
    public String getName() { return name; }

    @Override
    public long getSize() {
        return children.stream()
                       .mapToLong(FileSystemNode::getSize)
                       .sum();
    }

    @Override
    public void print(String indent) {
        System.out.println(indent + "📁 " + name);
        for (FileSystemNode child : children) {
            child.print(indent + "  ");
        }
    }
}

2. Practitioner Insights

2.1 The Transparency vs. Safety Trade-off

2.2 Recursion Depth

For extremely deep trees, recursive implementation of getSize() can lead to StackOverflowError. In such cases, use an iterative traversal with an explicit java.util.Deque as a stack.

2.3 Caching Results

In large, static trees, the overhead of calculating getSize() recursively can be significant. Implementing a "dirty" flag or caching the size at the Composite level improves performance for read-heavy workloads.

2.4 Synergy with Visitor

If you need to add many new operations (e.g., compress(), calculateChecksum()), use the Visitor Pattern to avoid polluting the FileSystemNode interface and breaking the Open/Closed Principle.