Composite Pattern
The Composite pattern lets you compose objects into tree structures and treat individual objects and groups of objects uniformly. A folder can contain files or other folders. A menu can contain items or sub-menus. The client doesnβt need to know whether itβs dealing with a leaf or a branch.
Why this matters: File systems, organizational hierarchies, menus, expression trees, and nested pricing rules all form trees. When the problem says βa group can contain other groups,β Composite is the answer.
Prerequisites
- SOLID Principles β Composite uses a common interface for SRP
- Inheritance vs Composition β despite the name, this is about tree structures
Class Diagram
classDiagram
class FileSystemNode {
<<interface>>
+getName() String
+getSize() long
+display(indent)
}
class File {
-name: String
-size: long
+getName() String
+getSize() long
+display(indent)
}
class Directory {
-name: String
-children: List~FileSystemNode~
+add(FileSystemNode)
+remove(FileSystemNode)
+getName() String
+getSize() long
+display(indent)
}
FileSystemNode <|.. File
FileSystemNode <|.. Directory
Directory o-- FileSystemNode
The key insight: Directory both implements FileSystemNode and contains a list of FileSystemNode. This recursion is what makes the tree structure work.
Real-World Example: File System
// Component interface
public interface FileSystemNode {
String getName();
long getSize();
void display(String indent);
String getPath();
}
// Leaf: a file has no children
public class File implements FileSystemNode {
private final String name;
private final long size;
private final String extension;
public File(String name, long size) {
this.name = name;
this.size = size;
this.extension = name.contains(".") ? name.substring(name.lastIndexOf('.')) : "";
}
@Override
public String getName() { return name; }
@Override
public long getSize() { return size; }
@Override
public void display(String indent) {
System.out.println(indent + name + " (" + formatSize(size) + ")");
}
@Override
public String getPath() { return name; }
}
// Composite: a directory contains other nodes (files or directories)
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);
}
public void remove(FileSystemNode node) {
children.remove(node);
}
public List<FileSystemNode> getChildren() {
return Collections.unmodifiableList(children);
}
@Override
public String getName() { return name; }
@Override
public long getSize() {
// Recursively compute total size
return children.stream()
.mapToLong(FileSystemNode::getSize)
.sum();
}
@Override
public void display(String indent) {
System.out.println(indent + name + "/");
for (FileSystemNode child : children) {
child.display(indent + " ");
}
}
@Override
public String getPath() { return name + "/"; }
// Search within the tree
public List<FileSystemNode> find(String pattern) {
List<FileSystemNode> results = new ArrayList<>();
for (FileSystemNode child : children) {
if (child.getName().matches(pattern)) {
results.add(child);
}
if (child instanceof Directory dir) {
results.addAll(dir.find(pattern));
}
}
return results;
}
}
// Usage: client treats files and directories uniformly
Directory root = new Directory("project");
Directory src = new Directory("src");
Directory tests = new Directory("tests");
src.add(new File("Main.java", 2048));
src.add(new File("Utils.java", 1024));
tests.add(new File("MainTest.java", 1536));
root.add(src);
root.add(tests);
root.add(new File("README.md", 512));
// Works the same whether node is File or Directory
System.out.println(root.getSize()); // 5120 (sum of all files)
root.display(""); // Shows full tree
from abc import ABC, abstractmethod
class FileSystemNode(ABC):
@abstractmethod
def get_name(self) -> str: ...
@abstractmethod
def get_size(self) -> int: ...
@abstractmethod
def display(self, indent: str = ""): ...
class File(FileSystemNode):
def __init__(self, name: str, size: int):
self._name = name
self._size = size
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return self._size
def display(self, indent: str = ""):
print(f"{indent}{self._name} ({self._size} bytes)")
class Directory(FileSystemNode):
def __init__(self, name: str):
self._name = name
self._children: list[FileSystemNode] = []
def add(self, node: FileSystemNode):
self._children.append(node)
def remove(self, node: FileSystemNode):
self._children.remove(node)
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return sum(child.get_size() for child in self._children)
def display(self, indent: str = ""):
print(f"{indent}{self._name}/")
for child in self._children:
child.display(indent + " ")
def find(self, pattern: str) -> list[FileSystemNode]:
results = []
for child in self._children:
if pattern in child.get_name():
results.append(child)
if isinstance(child, Directory):
results.extend(child.find(pattern))
return results
# Usage
root = Directory("project")
src = Directory("src")
src.add(File("main.py", 2048))
src.add(File("utils.py", 1024))
root.add(src)
root.add(File("README.md", 512))
print(root.get_size()) # 3584
root.display()
#include <string>
#include <vector>
#include <memory>
#include <iostream>
#include <numeric>
class FileSystemNode {
public:
virtual ~FileSystemNode() = default;
virtual string getName() const = 0;
virtual long getSize() const = 0;
virtual void display(const string& indent) const = 0;
};
class File : public FileSystemNode {
string name_;
long size_;
public:
File(string name, long size) : name_(std::move(name)), size_(size) {}
string getName() const override { return name_; }
long getSize() const override { return size_; }
void display(const string& indent) const override {
cout << indent << name_ << " (" << size_ << " bytes)" << endl;
}
};
class Directory : public FileSystemNode {
string name_;
vector<shared_ptr<FileSystemNode>> children_;
public:
explicit Directory(string name) : name_(std::move(name)) {}
void add(shared_ptr<FileSystemNode> node) {
children_.push_back(std::move(node));
}
string getName() const override { return name_; }
long getSize() const override {
long total = 0;
for (const auto& child : children_) {
total += child->getSize();
}
return total;
}
void display(const string& indent) const override {
cout << indent << name_ << "/" << endl;
for (const auto& child : children_) {
child->display(indent + " ");
}
}
};
// Usage
auto root = make_shared<Directory>("project");
auto src = make_shared<Directory>("src");
src->add(make_shared<File>("main.cpp", 2048));
src->add(make_shared<File>("utils.cpp", 1024));
root->add(src);
root->add(make_shared<File>("README.md", 512));
cout << root->getSize() << endl; // 3584
root->display("");
When to Use vs When to Avoid
| Use Composite When | Avoid When |
|---|---|
| You have a tree/hierarchy structure | Data is flat (just use a list) |
| Individual and group objects should be treated uniformly | Leaves and containers have very different interfaces |
| Recursive operations make sense (size, count, display) | No operations span the hierarchy |
| New node types might be added | The structure is simple and fixed |
Interview Questions
-
βHow do you handle operations that only make sense on leaves or composites?β β You can either throw
UnsupportedOperationExceptionfor invalid calls, or split into separate interfaces (safer but slightly more complex). -
βWhat about cycles?β β A directory containing itself would create infinite recursion. Guard against it with a visited set or by checking parentage before adding.
-
βComposite vs just using a tree data structure?β β Composite gives polymorphism. A generic tree makes you check node types everywhere. Composite lets you call
getSize()on any node without knowing if itβs a leaf or branch. -
βHow do you traverse the tree?β β Depth-first (display shows this naturally), breadth-first (use a queue), or with the Visitor pattern for complex operations.