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).
In a file system, both File and Directory share a common interface for operations like getSize() or list().
import java.util.List;
public interface FileSystemNode {
String getName();
long getSize();
void print(String indent);
}
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)");
}
}
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 + " ");
}
}
}
add()/remove() in the base interface. This allows clients to treat all nodes identically but forces Leaf nodes to throw UnsupportedOperationException.Composite class. This prevents runtime errors on Leaves but requires clients to cast or use type checking before modifying the tree.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.
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.
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.