Design Principle
Composite Pattern
Learn the Composite pattern: compose objects into tree structures to represent part-whole hierarchies. Treat individual objects and compositions uniformly.
The Composite Pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions uniformly.
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
// Component interface
interface FileSystemComponent {
getName(): string;
getSize(): number;
display(indent: string): void;
}
// Leaf (File)
class File implements FileSystemComponent {
constructor(private name: string, private size: number) {}
getName(): string {
return this.name;
}
getSize(): number {
return this.size;
}
display(indent: string): void {
console.log(`${indent}📄 ${this.name} (${this.size} bytes)`);
}
}
// Composite (Directory)
class Directory implements FileSystemComponent {
private children: FileSystemComponent[] = [];
constructor(private name: string) {}
getName(): string {
return this.name;
}
getSize(): number {
return this.children.reduce((total, child) => total + child.getSize(), 0);
}
add(component: FileSystemComponent): void {
this.children.push(component);
}
remove(component: FileSystemComponent): void {
const index = this.children.indexOf(component);
if (index > -1) {
this.children.splice(index, 1);
}
}
getChild(index: number): FileSystemComponent | null {
return this.children[index] || null;
}
display(indent: string): void {
console.log(`${indent}📁 ${this.name}/ (${this.getSize()} bytes)`);
this.children.forEach(child => {
child.display(indent + " ");
});
}
}
// Usage
const root = new Directory("root");
const documents = new Directory("documents");
const pictures = new Directory("pictures");
const file1 = new File("readme.txt", 1024);
const file2 = new File("notes.txt", 2048);
const file3 = new File("photo.jpg", 5120);
documents.add(file1);
documents.add(file2);
pictures.add(file3);
root.add(documents);
root.add(pictures);
root.display("");
// Output:
// 📁 root/ (8192 bytes)
// 📁 documents/ (3072 bytes)
// 📄 readme.txt (1024 bytes)
// 📄 notes.txt (2048 bytes)
// 📁 pictures/ (5120 bytes)
// 📄 photo.jpg (5120 bytes)
UI Component Example
// Component interface
interface UIComponent {
render(): void;
add(component: UIComponent): void;
remove(component: UIComponent): void;
getChild(index: number): UIComponent | null;
}
// Leaf (Button)
class Button implements UIComponent {
constructor(private label: string) {}
render(): void {
console.log(`<button>${this.label}</button>`);
}
add(component: UIComponent): void {
throw new Error("Cannot add to leaf");
}
remove(component: UIComponent): void {
throw new Error("Cannot remove from leaf");
}
getChild(index: number): UIComponent | null {
return null;
}
}
// Leaf (Text)
class Text implements UIComponent {
constructor(private content: string) {}
render(): void {
console.log(`<text>${this.content}</text>`);
}
add(component: UIComponent): void {
throw new Error("Cannot add to leaf");
}
remove(component: UIComponent): void {
throw new Error("Cannot remove from leaf");
}
getChild(index: number): UIComponent | null {
return null;
}
}
// Composite (Container)
class Container implements UIComponent {
private children: UIComponent[] = [];
constructor(private name: string) {}
render(): void {
console.log(`<div class="${this.name}">`);
this.children.forEach(child => child.render());
console.log(`</div>`);
}
add(component: UIComponent): void {
this.children.push(component);
}
remove(component: UIComponent): void {
const index = this.children.indexOf(component);
if (index > -1) {
this.children.splice(index, 1);
}
}
getChild(index: number): UIComponent | null {
return this.children[index] || null;
}
}
// Usage
const page = new Container("page");
const header = new Container("header");
const content = new Container("content");
const footer = new Container("footer");
header.add(new Text("Welcome"));
header.add(new Button("Login"));
content.add(new Text("Main content here"));
footer.add(new Text("Copyright 2024"));
page.add(header);
page.add(content);
page.add(footer);
page.render();
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:
// Both File and Directory implement same interface
interface FileSystemComponent {
getSize(): number;
display(): void;
}
// Leaf
class File implements FileSystemComponent { /* ... */ }
// Composite
class Directory implements FileSystemComponent {
private children: FileSystemComponent[] = [];
// Can contain Files or other Directories
}
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:
interface Component {
operation(): void;
add(component: Component): void;
remove(component: Component): void;
}
2. Leaf:
class Leaf implements Component {
operation(): void {
// Leaf operation
}
add(component: Component): void {
throw new Error("Cannot add to leaf");
}
remove(component: Component): void {
throw new Error("Cannot remove from leaf");
}
}
3. Composite:
class Composite implements Component {
private children: Component[] = [];
operation(): void {
// Operate on all children
this.children.forEach(child => child.operation());
}
add(component: Component): void {
this.children.push(component);
}
remove(component: Component): void {
// Remove logic
}
}
Handling Leaf Operations:
Option 1: Throw Exception
class Leaf implements Component {
add(component: Component): void {
throw new Error("Cannot add to leaf");
}
}
Option 2: Do Nothing
class Leaf implements Component {
add(component: Component): void {
// Do nothing
}
}
Option 3: Null Object Pattern
class NullComponent implements Component {
add(component: Component): void {
// Do nothing
}
}
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:
// Component interface
interface DocumentComponent {
getWordCount(): number;
export(format: ExportFormat): string;
format(style: Style): void;
add(component: DocumentComponent): void;
remove(component: DocumentComponent): void;
}
// Leaf - Paragraph
class Paragraph implements DocumentComponent {
private text: string = "";
private style: Style | null = null;
constructor(text: string) {
this.text = text;
}
getWordCount(): number {
return this.text.split(/\s+/).filter(word => word.length > 0).length;
}
export(format: ExportFormat): string {
switch (format) {
case "html":
return `<p>${this.text}</p>`;
case "markdown":
return this.text;
case "plain":
return this.text;
default:
return this.text;
}
}
format(style: Style): void {
this.style = style;
}
add(component: DocumentComponent): void {
throw new Error("Cannot add to paragraph");
}
remove(component: DocumentComponent): void {
throw new Error("Cannot remove from paragraph");
}
}
// Leaf - Image
class Image implements DocumentComponent {
constructor(private src: string, private alt: string) {}
getWordCount(): number {
return 0; // Images don't contribute to word count
}
export(format: ExportFormat): string {
switch (format) {
case "html":
return `<img src="${this.src}" alt="${this.alt}">`;
case "markdown":
return ``;
default:
return `[Image: ${this.alt}]`;
}
}
format(style: Style): void {
// Apply image-specific formatting
}
add(component: DocumentComponent): void {
throw new Error("Cannot add to image");
}
remove(component: DocumentComponent): void {
throw new Error("Cannot remove from image");
}
}
// Composite - Section
class Section implements DocumentComponent {
private children: DocumentComponent[] = [];
private title: string;
constructor(title: string) {
this.title = title;
}
getWordCount(): number {
return this.children.reduce((total, child) => total + child.getWordCount(), 0);
}
export(format: ExportFormat): string {
const childrenContent = this.children
.map(child => child.export(format))
.join("\n");
switch (format) {
case "html":
return `<section><h2>${this.title}</h2>${childrenContent}</section>`;
case "markdown":
return `## ${this.title}\n\n${childrenContent}`;
default:
return `${this.title}\n${childrenContent}`;
}
}
format(style: Style): void {
this.children.forEach(child => child.format(style));
}
add(component: DocumentComponent): void {
this.children.push(component);
}
remove(component: DocumentComponent): void {
const index = this.children.indexOf(component);
if (index > -1) {
this.children.splice(index, 1);
}
}
}
// Composite - Document
class Document implements DocumentComponent {
private children: DocumentComponent[] = [];
getWordCount(): number {
return this.children.reduce((total, child) => total + child.getWordCount(), 0);
}
export(format: ExportFormat): string {
return this.children
.map(child => child.export(format))
.join("\n\n");
}
format(style: Style): void {
this.children.forEach(child => child.format(style));
}
add(component: DocumentComponent): void {
this.children.push(component);
}
remove(component: DocumentComponent): void {
const index = this.children.indexOf(component);
if (index > -1) {
this.children.splice(index, 1);
}
}
}
// Usage
const doc = new Document();
const intro = new Section("Introduction");
intro.add(new Paragraph("This is the introduction."));
intro.add(new Image("intro.jpg", "Introduction image"));
const body = new Section("Body");
body.add(new Paragraph("Main content here."));
body.add(new Paragraph("More content."));
doc.add(intro);
doc.add(body);
console.log(`Word count: ${doc.getWordCount()}`);
console.log(doc.export("html"));
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
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
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.