Composite Pattern
The Composite Pattern lets you compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.
This pattern is especially useful when you need to work with tree-like object structures, and when you want clients to be able to ignore the differences between composite objects (branches) and individual objects (leaves).
Problem
Imagine you're building a file system where you have files and directories. Both files and directories need to be treated as file system items. Files are simple, but directories can contain other directories and files. How do you design the system so that clients can work with both simple and complex elements through the same interface?
Components
- Component: Declares the interface for objects in the composition and implements default behavior for the interface common to all classes.
- Leaf: Represents leaf objects in the composition. A leaf has no children.
- Composite: Defines behavior for components having children and stores child components.
- Client: Manipulates objects in the composition through the Component interface.
Implementation Example
Let's implement a file system with files and directories using the Composite pattern:
// Component - The common interface
class FileSystemItem {
constructor(name) {
this.name = name;
}
// Common methods that all file system items should implement
getName() {
return this.name;
}
getSize() {
// To be implemented by subclasses
}
print(indent = 0) {
// To be implemented by subclasses
}
}
// Leaf - File class
class File extends FileSystemItem {
constructor(name, size) {
super(name);
this.size = size; // Size in KB
}
getSize() {
return this.size;
}
print(indent = 0) {
console.log(`${' '.repeat(indent)}📄 ${this.name} (${this.size}KB)`);
}
}
// Composite - Directory class
class Directory extends FileSystemItem {
constructor(name) {
super(name);
this.children = [];
}
add(item) {
this.children.push(item);
return this;
}
remove(item) {
const index = this.children.indexOf(item);
if (index !== -1) {
this.children.splice(index, 1);
}
}
getSize() {
// Calculate total size by summing the size of all children
return this.children.reduce((sum, child) => sum + child.getSize(), 0);
}
print(indent = 0) {
console.log(`${' '.repeat(indent)}📁 ${this.name} (${this.getSize()}KB)`);
// Print all children with increased indentation
this.children.forEach(child => {
child.print(indent + 2);
});
}
}
With this pattern, both Files and Directories share the same interface (FileSystemItem), but directories can also contain other items. Clients can work with both individual files and complex directory structures through the same interface.
Interactive Demo: File System Explorer
Explore a file system structure built with the Composite pattern. You can expand/collapse folders, calculate sizes, and add new items.
When to Use
- When you want to represent part-whole hierarchies of objects
- When you want clients to be able to ignore the difference between compositions of objects and individual objects
- When the structure can have any level of complexity and is dynamic
Benefits
- Simplifies client code by allowing it to treat composite structures and individual objects uniformly
- Makes it easier to add new types of components as the client code works with the abstract interface
- Creates a natural recursive structure where operations can propagate through the whole hierarchy
Real-World Uses
- File systems (files and directories)
- Graphical user interfaces (containers and components)
- Organizational structures (departments and employees)
- Document object models (XML/HTML document structures)
- Menu systems with submenus