Design Principle
Composite Pattern
Learn the Composite pattern: compose objects into tree structures to represent part-whole hierarchies. Treat individual objects and compositions uniformly.
Composite Pattern
Why This Matters
Think of the Composite pattern like a file system. You have files (individual objects) and folders (composite objects). Folders can contain files or other folders. But from the outside, you treat them the same—you can get the size of a file or a folder (which sums up its contents). The Composite pattern does the same for code—it lets you treat individual objects and groups of objects uniformly.
This matters because many systems have hierarchical structures: file systems, UI components (containers and widgets), organization charts, expression trees. The Composite pattern provides a clean way to represent these hierarchies and operate on them uniformly. Without it, you'd need separate code for individual objects and groups.
In interviews, when someone asks "How would you represent a tree structure?", they're testing whether you understand the Composite pattern. Do you know how to treat individual and composite objects uniformly? Can you build recursive structures? Most engineers can't. They use separate classes and wonder why the code is complex.
What Engineers Usually Get Wrong
Engineers often think "Composite pattern is just tree structures." But Composite pattern is more specific—it's about treating individual and composite objects uniformly through a common interface. This allows recursive operations (like calculating total size of a folder by summing its contents) without the client knowing whether it's dealing with a file or a folder.
Engineers also don't understand when to use Composite. Use Composite when you have part-whole hierarchies and want to treat individual and composite objects uniformly. Don't use it for simple collections—use regular collections instead. Composite adds complexity, so use it only when you need the uniform interface.
How This Breaks Systems in the Real World
A service was building a UI component system. They had separate classes for individual components and containers. Code that operated on components had to check "is this a container?" and handle containers differently. The code was complex and error-prone. The fix? Use Composite pattern. Create a common interface for components and containers. Now code can operate on both uniformly, without special cases.
Another story: A service was using Composite pattern but didn't handle cycles. A folder could contain itself (directly or indirectly), creating infinite loops. Operations like "calculate total size" would hang. The fix? Detect cycles. Use visited sets or depth limits to prevent infinite recursion.
What is the Composite Pattern?
Composite Pattern provides:
- Tree structure: Represent hierarchical structures
- Uniform treatment: Treat individual and composite objects the same
- Recursive composition: Composites can contain other composites
- Simplified client code: Client doesn't need to distinguish between leaf and composite
Use cases:
- File system (files and folders)
- UI components (containers and widgets)
- Organization structures
- Expression trees
Structure
Component (interface)
├─ operation()
├─ add(component)
├─ remove(component)
└─ getChild(index)
Leaf (implements Component)
└─ operation()
Composite (implements Component)
└─ children: Component[]
└─ operation() (calls operation on all children)
└─ add(component)
└─ remove(component)
Examples
File System Example
1// Component interface2interface FileSystemComponent {3 getName(): string;4 getSize(): number;5 display(indent: string): void;6}78// Leaf (File)9class File implements FileSystemComponent {10 constructor(private name: string, private size: number) {}1112 getName(): string {13 return this.name;14 }
UI Component Example
1// Component interface2interface UIComponent {3 render(): void;4 add(component: UIComponent): void;5 remove(component: UIComponent): void;6 getChild(index: number): UIComponent | null;7}89// Leaf (Button)10class Button implements UIComponent {11 constructor(private label: string) {}1213 render(): void {
Common Pitfalls
- Leaf operations: Adding operations to leaf that don't make sense. Fix: Use null object pattern or throw exceptions
- Performance: Traversing large trees can be slow. Fix: Use caching, lazy evaluation
- Circular references: Composites can reference themselves. Fix: Add cycle detection
- Too many operations: Component interface becomes too large. Fix: Use visitor pattern for complex operations
Interview Questions
Beginner
Q: What is the Composite pattern and when would you use it?
A:
Composite Pattern composes objects into tree structures to represent part-whole hierarchies.
Key characteristics:
- Tree structure: Represents hierarchical structures
- Uniform treatment: Treat individual and composite objects the same
- Recursive composition: Composites can contain other composites
- Simplified client: Client doesn't distinguish between leaf and composite
Example:
1// Both File and Directory implement same interface2interface FileSystemComponent {3 getSize(): number;4 display(): void;5}67// Leaf8class File implements FileSystemComponent { /* ... */ }910// Composite11class Directory implements FileSystemComponent {12 private children: FileSystemComponent[] = [];13 // Can contain Files or other Directories14}
Use cases:
- File systems: Files and folders
- UI components: Containers and widgets
- Organization structures: Departments and employees
- Expression trees: Operations and operands
Intermediate
Q: Explain how the Composite pattern works. How do you handle operations that don't apply to leaves?
A:
Composite Pattern Structure:
1. Component Interface:
1interface Component {2 operation(): void;3 add(component: Component): void;4 remove(component: Component): void;5}
2. Leaf:
1class Leaf implements Component {2 operation(): void {3 // Leaf operation4 }56 add(component: Component): void {7 throw new Error("Cannot add to leaf");8 }910 remove(component: Component): void {11 throw new Error("Cannot remove from leaf");12 }13}
3. Composite:
1class Composite implements Component {2 private children: Component[] = [];34 operation(): void {5 // Operate on all children6 this.children.forEach(child => child.operation());7 }89 add(component: Component): void {10 this.children.push(component);11 }1213 remove(component: Component): void
Handling Leaf Operations:
Option 1: Throw Exception
1class Leaf implements Component {2 add(component: Component): void {3 throw new Error("Cannot add to leaf");4 }5}
Option 2: Do Nothing
1class Leaf implements Component {2 add(component: Component): void {3 // Do nothing4 }5}
Option 3: Null Object Pattern
1class NullComponent implements Component {2 add(component: Component): void {3 // Do nothing4 }5}
Senior
Q: Design a composite system for a document editor that supports nested sections, paragraphs, images, and tables. Handle operations like word count, export, and formatting that work differently for different component types.
A:
1// Component interface2interface DocumentComponent {3 getWordCount(): number;4 export(format: ExportFormat): string;5 format(style: Style): void;6 add(component: DocumentComponent): void;7 remove(component: DocumentComponent): void;8}910// Leaf - Paragraph11class Paragraph implements DocumentComponent {12 private text: string = "";13 private style: Style |
Features:
- Uniform interface: All components implement same interface
- Type-specific behavior: Each type handles operations differently
- Recursive operations: Operations propagate through tree
- Flexible composition: Mix different component types
-
Composite pattern: Represents part-whole hierarchies
-
Uniform treatment: Treat individual and composite objects the same
-
Tree structure: Builds tree structures recursively
-
Use cases: File systems, UI components, document structures
-
Leaf operations: Handle operations that don't apply to leaves
-
Best practices: Use exceptions or null object for leaf operations, consider performance
-
Trees - Composite pattern is based on tree structures. Understanding trees helps understand composite pattern.
-
Recursion & Backtracking - Composite operations are naturally recursive. Understanding recursion helps implement composite operations.
-
SOLID Principles - Composite pattern follows Open/Closed Principle (extend without modify). Understanding SOLID helps understand composite benefits.
-
Separation of Concerns - Composite pattern treats individual and composite uniformly. Understanding separation of concerns helps understand composite benefits.
-
Decorator Pattern - Both patterns compose objects. Composite composes tree structures, Decorator composes features.
Key Takeaways
Composite pattern: Represents part-whole hierarchies
Uniform treatment: Treat individual and composite objects the same
Tree structure: Builds tree structures recursively
Use cases: File systems, UI components, document structures
Leaf operations: Handle operations that don't apply to leaves
Best practices: Use exceptions or null object for leaf operations, consider performance
Related Topics
Trees
Composite pattern is based on tree structures. Understanding trees helps understand composite pattern.
Recursion & Backtracking
Composite operations are naturally recursive. Understanding recursion helps implement composite operations.
SOLID Principles
Composite pattern follows Open/Closed Principle (extend without modify). Understanding SOLID helps understand composite benefits.
Separation of Concerns
Composite pattern treats individual and composite uniformly. Understanding separation of concerns helps understand composite benefits.
Decorator Pattern
Both patterns compose objects. Composite composes tree structures, Decorator composes features.
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.