Composite
Core. Expect to meet this one, and expect to be asked for it by name.
Start with the problem
An in-memory file system needs to answer ls: given a directory, list what is inside it. A directory is really only a name mapped to whatever is under it, so a Map is the obvious way to hold one.
final class DirectoryNode {
private final TreeMap<String, Object> children = new TreeMap<>();
List<String> childNames() {
return List.copyOf(children.keySet());
}
}
Object as the value type is honest about the actual problem: a child can be a file, holding content, or another directory, holding more children of its own.
Watch where it goes
Reading a child back out means asking what it actually is before doing anything with it, since Object remembers nothing. Every caller that walks the tree ends up writing the same instanceof check: is this a file, in which case read its content, or a directory, in which case recurse into it. Copy that check into read, into mv, into delete, and each one has to stay in sync with the same two-way decision.
The deeper cost is that nothing stops a third kind of thing from turning up in the map. Object accepts a String, an Integer, or a raw List as readily as it accepts a real file or directory. None of the code that reads children back out would catch that until it actually ran.
The move
Give files and directories a common type that names exactly the two things a node can be, and nothing else.
sealed interface Node permits FileNode, DirectoryNode {}
final class FileNode implements Node {
private String content;
FileNode(String content) {
this.content = content;
}
String content() {
return content;
}
}
final class DirectoryNode implements Node {
private final TreeMap<String, Node> children = new TreeMap<>();
Node child(String name) {
return children.get(name);
}
void addChild(String name, Node node) {
if (children.putIfAbsent(name, node) != null) {
throw new IllegalStateException("directory already has a child named '" + name + "'");
}
}
}
This is corpus/file-system's shape. A DirectoryNode holds Node children, and each of those children can itself be a DirectoryNode holding more children, which is what lets a tree of any depth live in the same two classes. sealed closes the set: nothing outside FileNode and DirectoryNode can ever implement Node, so the compiler, not a runtime check, guarantees there is no third kind to worry about.
What modern Java changes here
The original catalogue reaches for one virtual method on the common type, overridden by leaf and composite alike, so a caller can call component.operation() without asking which one it has. corpus/file-system takes a different route on purpose: Nodes.kindOf pattern-matches over Node with a switch, once, in one small class.
static EntryKind kindOf(Node node) {
return switch (node) {
case FileNode f -> EntryKind.FILE;
case DirectoryNode d -> EntryKind.DIRECTORY;
};
}
A sealed type turns that switch exhaustive. The compiler refuses to build this class if a case is missing, and refuses to build it at all once a third kind of node exists without a case added for it. That is the same safety a virtual method gives, reached from the other direction. Virtual dispatch suits an operation every node type has to answer, like du() walking to a total size. Pattern matching suits an occasional question asked from outside the hierarchy, like kindOf, with no new method forced onto Node itself.
When naming it is wrong
A tree with exactly one kind of node, or a fixed depth known in advance, does not need a common interface at all. A file system with no subdirectories, a flat list of files and nothing more, is a Map from name to content. Wrapping that in a Node hierarchy in case directories arrive later is a guess dressed as design.
The threshold: reach for this once a real recursive structure exists, where a container can hold another container of the same kind. Code also needs to treat a leaf and a container the same way for at least one operation. A structure that is provably flat, with nesting nobody has asked for, scores as over-engineered (premature interface) under the Standard's D3 dimension.
Where this lives in the app
The file-system corpus problem builds this out directly: Node, FileNode, and DirectoryNode in corpus/file-system/reference/src are the reference solution's actual composite structure, and Nodes.kindOf is the one place that pattern-matches over it.