Loading learning content...
The Composite Pattern's theoretical elegance is evident, but its true value emerges in practical application. This pattern appears throughout software engineering—in frameworks you use daily, in architectures of major systems, and in domains ranging from user interfaces to financial calculations.
In this page, we'll explore comprehensive, real-world examples that demonstrate the Composite Pattern solving actual problems. Each example is complete enough to serve as a template for your own implementations.
By the end of this page, you will see the Composite Pattern applied across multiple domains: file systems, UI component trees, graphics systems, organizational hierarchies, menu systems, and expression trees. You'll understand when and why to choose this pattern, and have practical templates to draw from.
The file system is the quintessential Composite Pattern example. Every operating system's file system is built on this pattern, enabling uniform operations on files and directories.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
// ============================================// Component: Abstract file system entry// ============================================ interface FileSystemEntry { getName(): string; getPath(): string; getSize(): number; getCreatedAt(): Date; getModifiedAt(): Date; // Tree operations find(name: string): FileSystemEntry | null; findAll(predicate: (entry: FileSystemEntry) => boolean): FileSystemEntry[]; forEach(callback: (entry: FileSystemEntry) => void): void; // Child management add(entry: FileSystemEntry): void; remove(entry: FileSystemEntry): void; getChildren(): FileSystemEntry[]; isDirectory(): boolean; // Navigation getParent(): FileSystemEntry | null; setParent(parent: FileSystemEntry | null): void;} // ============================================// Leaf: File// ============================================ class File implements FileSystemEntry { private parent: FileSystemEntry | null = null; private createdAt: Date = new Date(); private modifiedAt: Date = new Date(); constructor( private name: string, private content: string, private size: number ) {} getName(): string { return this.name; } getPath(): string { if (this.parent) { return `${this.parent.getPath()}/${this.name}`; } return this.name; } getSize(): number { return this.size; } getCreatedAt(): Date { return this.createdAt; } getModifiedAt(): Date { return this.modifiedAt; } find(name: string): FileSystemEntry | null { return this.name === name ? this : null; } findAll(predicate: (entry: FileSystemEntry) => boolean): FileSystemEntry[] { return predicate(this) ? [this] : []; } forEach(callback: (entry: FileSystemEntry) => void): void { callback(this); } // Leaf doesn't support children add(entry: FileSystemEntry): void { throw new Error(`Cannot add to file: ${this.name}`); } remove(entry: FileSystemEntry): void { throw new Error(`Cannot remove from file: ${this.name}`); } getChildren(): FileSystemEntry[] { return []; } isDirectory(): boolean { return false; } getParent(): FileSystemEntry | null { return this.parent; } setParent(parent: FileSystemEntry | null): void { this.parent = parent; } // File-specific methods getContent(): string { return this.content; } setContent(content: string): void { this.content = content; this.size = content.length; this.modifiedAt = new Date(); }} // ============================================// Composite: Directory// ============================================ class Directory implements FileSystemEntry { private children: FileSystemEntry[] = []; private parent: FileSystemEntry | null = null; private createdAt: Date = new Date(); private modifiedAt: Date = new Date(); constructor(private name: string) {} getName(): string { return this.name; } getPath(): string { if (this.parent) { return `${this.parent.getPath()}/${this.name}`; } return this.name; } getSize(): number { // Directory size = sum of all children (recursive) return this.children.reduce((sum, child) => sum + child.getSize(), 0); } getCreatedAt(): Date { return this.createdAt; } getModifiedAt(): Date { return this.modifiedAt; } find(name: string): FileSystemEntry | null { if (this.name === name) return this; for (const child of this.children) { const found = child.find(name); if (found) return found; } return null; } findAll(predicate: (entry: FileSystemEntry) => boolean): FileSystemEntry[] { const results: FileSystemEntry[] = predicate(this) ? [this] : []; for (const child of this.children) { results.push(...child.findAll(predicate)); } return results; } forEach(callback: (entry: FileSystemEntry) => void): void { callback(this); for (const child of this.children) { child.forEach(callback); } } add(entry: FileSystemEntry): void { // Check for duplicate name if (this.children.some(c => c.getName() === entry.getName())) { throw new Error(`Entry named "${entry.getName()}" already exists`); } // Remove from old parent if any const oldParent = entry.getParent(); if (oldParent) { oldParent.remove(entry); } entry.setParent(this); this.children.push(entry); this.modifiedAt = new Date(); } remove(entry: FileSystemEntry): void { const index = this.children.indexOf(entry); if (index !== -1) { entry.setParent(null); this.children.splice(index, 1); this.modifiedAt = new Date(); } } getChildren(): FileSystemEntry[] { return [...this.children]; } isDirectory(): boolean { return true; } getParent(): FileSystemEntry | null { return this.parent; } setParent(parent: FileSystemEntry | null): void { this.parent = parent; } // Directory-specific methods getFileCount(): number { return this.findAll(e => !e.isDirectory()).length; } getDirectoryCount(): number { return this.findAll(e => e.isDirectory()).length - 1; // Exclude self }} // ============================================// Usage Example// ============================================ const root = new Directory("project");const src = new Directory("src");const tests = new Directory("tests");const readme = new File("README.md", "# Project\n...", 256);const index = new File("index.ts", "export * from './app';", 48);const app = new File("app.ts", "// main app code", 1024);const test1 = new File("app.test.ts", "describe('App'...", 512); root.add(src);root.add(tests);root.add(readme);src.add(index);src.add(app);tests.add(test1); // Uniform operationsconsole.log(`Total size: ${root.getSize()} bytes`); // 1840console.log(`Files: ${root.getFileCount()}`); // 4console.log(`Subdirs: ${root.getDirectoryCount()}`); // 2 // Find all TypeScript filesconst tsFiles = root.findAll(e => e.getName().endsWith(".ts"));console.log(`TS files: ${tsFiles.map(f => f.getName()).join(", ")}`);// Output: index.ts, app.ts, app.test.tsEvery major UI framework—from React to Swing to WPF—uses the Composite Pattern. The UI is a tree of components: containers hold other components, and operations like render, layout, and event handling propagate through the tree.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
// ============================================// Component: Abstract UI Widget// ============================================ interface UIComponent { render(): string; getBounds(): { x: number; y: number; width: number; height: number }; setPosition(x: number, y: number): void; setSize(width: number, height: number): void; // Event handling handleEvent(event: UIEvent): boolean; // Tree structure add(component: UIComponent): void; remove(component: UIComponent): void; getChildren(): UIComponent[];} interface UIEvent { type: 'click' | 'keydown' | 'focus' | 'blur'; target: UIComponent;} // ============================================// Base class for common functionality// ============================================ abstract class BaseUIComponent implements UIComponent { protected x: number = 0; protected y: number = 0; protected width: number = 100; protected height: number = 50; abstract render(): string; getBounds() { return { x: this.x, y: this.y, width: this.width, height: this.height }; } setPosition(x: number, y: number): void { this.x = x; this.y = y; } setSize(width: number, height: number): void { this.width = width; this.height = height; } handleEvent(event: UIEvent): boolean { // Default: event not handled return false; } // Default: no children (leaves) add(component: UIComponent): void { throw new Error("Cannot add children to this component"); } remove(component: UIComponent): void { throw new Error("Cannot remove children from this component"); } getChildren(): UIComponent[] { return []; }} // ============================================// Leaf Components// ============================================ class Button extends BaseUIComponent { constructor( private label: string, private onClick: () => void ) { super(); } render(): string { return `<button style="position:absolute;left:${this.x}px;top:${this.y}px;width:${this.width}px;height:${this.height}px">${this.label}</button>`; } handleEvent(event: UIEvent): boolean { if (event.type === 'click' && event.target === this) { this.onClick(); return true; // Event handled } return false; }} class Label extends BaseUIComponent { constructor(private text: string) { super(); } render(): string { return `<span style="position:absolute;left:${this.x}px;top:${this.y}px">${this.text}</span>`; }} class TextInput extends BaseUIComponent { private value: string = ""; constructor(private placeholder: string) { super(); } render(): string { return `<input type="text" placeholder="${this.placeholder}" value="${this.value}" style="position:absolute;left:${this.x}px;top:${this.y}px;width:${this.width}px" />`; } getValue(): string { return this.value; } setValue(value: string): void { this.value = value; }} // ============================================// Composite: Panel (Container)// ============================================ class Panel extends BaseUIComponent { private children: UIComponent[] = []; private title: string; constructor(title: string = "") { super(); this.title = title; } render(): string { // Render self and all children const childrenHtml = this.children.map(c => c.render()).join("\n"); return `<div class="panel" style="position:absolute;left:${this.x}px;top:${this.y}px;width:${this.width}px;height:${this.height}px;border:1px solid #ccc;"> ${this.title ? `<div class="panel-title">${this.title}</div>` : ""} <div class="panel-content"> ${childrenHtml} </div></div>`; } handleEvent(event: UIEvent): boolean { // First, try to handle in children (event bubbling) for (const child of this.children) { if (child.handleEvent(event)) { return true; } } // Then, handle ourselves if needed return false; } 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); } } getChildren(): UIComponent[] { return [...this.children]; }} // ============================================// Composite: Window (Top-level container)// ============================================ class Window extends Panel { private isVisible: boolean = true; constructor(title: string) { super(title); this.setSize(400, 300); } render(): string { if (!this.isVisible) return ""; return `<div class="window" style="position:absolute;left:${this.x}px;top:${this.y}px;width:${this.width}px;height:${this.height}px;border:2px solid #333;background:white;"> <div class="window-titlebar">${this.title}</div> <div class="window-content"> ${this.getChildren().map(c => c.render()).join("\n")} </div></div>`; } show(): void { this.isVisible = true; } hide(): void { this.isVisible = false; }} // ============================================// Usage: Build a login form// ============================================ const loginWindow = new Window("Login");loginWindow.setPosition(100, 100); const formPanel = new Panel();formPanel.setSize(350, 200); const usernameLabel = new Label("Username:");usernameLabel.setPosition(20, 20); const usernameInput = new TextInput("Enter username");usernameInput.setPosition(100, 20);usernameInput.setSize(200, 30); const passwordLabel = new Label("Password:");passwordLabel.setPosition(20, 60); const passwordInput = new TextInput("Enter password");passwordInput.setPosition(100, 60);passwordInput.setSize(200, 30); const loginButton = new Button("Login", () => { console.log("Login clicked!");});loginButton.setPosition(100, 100);loginButton.setSize(100, 35); // Compose the treeformPanel.add(usernameLabel);formPanel.add(usernameInput);formPanel.add(passwordLabel);formPanel.add(passwordInput);formPanel.add(loginButton);loginWindow.add(formPanel); // Single call renders entire treeconsole.log(loginWindow.render());React's component tree, DOM elements, WPF's Visual Tree, Java Swing's JComponent hierarchy — all implement variations of the Composite Pattern. Understanding this pattern helps you understand how ALL UI frameworks work at a fundamental level.
Vector graphics applications like Adobe Illustrator, Figma, and SVG renderers use the Composite Pattern extensively. Shapes can be grouped, and groups can contain other groups, forming unlimited hierarchies.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
interface Graphic { draw(context: CanvasRenderingContext2D): void; move(dx: number, dy: number): void; scale(factor: number): void; rotate(degrees: number): void; getBoundingBox(): { x: number; y: number; width: number; height: number }; clone(): Graphic; // Group operations add(graphic: Graphic): void; remove(graphic: Graphic): void; getChildren(): Graphic[];} abstract class Shape implements Graphic { protected x: number = 0; protected y: number = 0; protected rotation: number = 0; protected scaleX: number = 1; protected scaleY: number = 1; protected fillColor: string = "black"; protected strokeColor: string = "none"; abstract draw(context: CanvasRenderingContext2D): void; abstract getBoundingBox(): { x: number; y: number; width: number; height: number }; abstract clone(): Graphic; move(dx: number, dy: number): void { this.x += dx; this.y += dy; } scale(factor: number): void { this.scaleX *= factor; this.scaleY *= factor; } rotate(degrees: number): void { this.rotation = (this.rotation + degrees) % 360; } // Leaves can't have children add(graphic: Graphic): void { throw new Error("Cannot add to shape"); } remove(graphic: Graphic): void { throw new Error("Cannot remove from shape"); } getChildren(): Graphic[] { return []; } protected setFill(color: string): this { this.fillColor = color; return this; }} // ============================================// Leaf: Circle// ============================================ class Circle extends Shape { constructor(x: number, y: number, private radius: number) { super(); this.x = x; this.y = y; } draw(ctx: CanvasRenderingContext2D): void { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rotation * Math.PI / 180); ctx.scale(this.scaleX, this.scaleY); ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, Math.PI * 2); ctx.fillStyle = this.fillColor; ctx.fill(); ctx.restore(); } getBoundingBox() { const r = this.radius * Math.max(this.scaleX, this.scaleY); return { x: this.x - r, y: this.y - r, width: r * 2, height: r * 2 }; } clone(): Graphic { const c = new Circle(this.x, this.y, this.radius); c.fillColor = this.fillColor; c.rotation = this.rotation; c.scaleX = this.scaleX; c.scaleY = this.scaleY; return c; }} // ============================================// Leaf: Rectangle// ============================================ class Rectangle extends Shape { constructor( x: number, y: number, private width: number, private height: number ) { super(); this.x = x; this.y = y; } draw(ctx: CanvasRenderingContext2D): void { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rotation * Math.PI / 180); ctx.scale(this.scaleX, this.scaleY); ctx.fillStyle = this.fillColor; ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height); ctx.restore(); } getBoundingBox() { const w = this.width * this.scaleX; const h = this.height * this.scaleY; return { x: this.x - w/2, y: this.y - h/2, width: w, height: h }; } clone(): Graphic { const r = new Rectangle(this.x, this.y, this.width, this.height); r.fillColor = this.fillColor; r.rotation = this.rotation; r.scaleX = this.scaleX; r.scaleY = this.scaleY; return r; }} // ============================================// Composite: Group// ============================================ class Group implements Graphic { private children: Graphic[] = []; draw(ctx: CanvasRenderingContext2D): void { // Draw all children - order matters (back to front) for (const child of this.children) { child.draw(ctx); } } move(dx: number, dy: number): void { // Moving a group moves all its children for (const child of this.children) { child.move(dx, dy); } } scale(factor: number): void { // Scaling a group scales all children for (const child of this.children) { child.scale(factor); } } rotate(degrees: number): void { // Rotating a group rotates all children // Note: For true group rotation, you'd need more complex transformation for (const child of this.children) { child.rotate(degrees); } } getBoundingBox() { if (this.children.length === 0) { return { x: 0, y: 0, width: 0, height: 0 }; } // Compute bounding box that contains all children let minX = Infinity, minY = Infinity; let maxX = -Infinity, maxY = -Infinity; for (const child of this.children) { const box = child.getBoundingBox(); minX = Math.min(minX, box.x); minY = Math.min(minY, box.y); maxX = Math.max(maxX, box.x + box.width); maxY = Math.max(maxY, box.y + box.height); } return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } clone(): Graphic { const group = new Group(); for (const child of this.children) { group.add(child.clone()); } return group; } add(graphic: Graphic): void { this.children.push(graphic); } remove(graphic: Graphic): void { const index = this.children.indexOf(graphic); if (index !== -1) { this.children.splice(index, 1); } } getChildren(): Graphic[] { return [...this.children]; } // Bring child to front (top of drawing order) bringToFront(graphic: Graphic): void { this.remove(graphic); this.children.push(graphic); } // Send child to back (bottom of drawing order) sendToBack(graphic: Graphic): void { this.remove(graphic); this.children.unshift(graphic); }} // ============================================// Usage: Create a scene// ============================================ const scene = new Group(); // Create a smiley face as a groupconst face = new Group();const head = new Circle(100, 100, 50);head.setFill("#FFDD00"); const leftEye = new Circle(85, 90, 8);leftEye.setFill("black"); const rightEye = new Circle(115, 90, 8);rightEye.setFill("black"); face.add(head);face.add(leftEye);face.add(rightEye); // Create a house as a groupconst house = new Group();const body = new Rectangle(300, 150, 80, 60);body.setFill("brown"); const roof = new Triangle(300, 100, 50); // Assuming Triangle existsroof.setFill("red"); house.add(body);// house.add(roof); // Add both groups to scenescene.add(face);scene.add(house); // Single call to draw everything// scene.draw(canvasContext); // Move entire face groupface.move(50, 0); // Scale entire scenescene.scale(1.5);Organizations have natural hierarchies: departments contain teams, teams contain employees. The Composite Pattern enables uniform operations like budget calculation, headcount, and reporting across the entire structure.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
interface OrganizationalUnit { getName(): string; getHeadCount(): number; getSalaryCost(): number; getBudget(): number; // Reporting generateReport(): string; findByName(name: string): OrganizationalUnit | null; // Structure add(unit: OrganizationalUnit): void; remove(unit: OrganizationalUnit): void; getSubUnits(): OrganizationalUnit[];} // ============================================// Leaf: Employee// ============================================ class Employee implements OrganizationalUnit { constructor( private name: string, private role: string, private salary: number ) {} getName(): string { return this.name; } getHeadCount(): number { return 1; } getSalaryCost(): number { return this.salary; } getBudget(): number { return 0; } // Employees don't have budgets generateReport(): string { return ` - ${this.name} (${this.role}): $${this.salary.toLocaleString()}`; } findByName(name: string): OrganizationalUnit | null { return this.name.toLowerCase().includes(name.toLowerCase()) ? this : null; } // Employees can't have subordinates add(unit: OrganizationalUnit): void { throw new Error("Employees cannot have subordinates"); } remove(unit: OrganizationalUnit): void { throw new Error("Employees cannot have subordinates"); } getSubUnits(): OrganizationalUnit[] { return []; }} // ============================================// Composite: Department / Team// ============================================ class Department implements OrganizationalUnit { private subUnits: OrganizationalUnit[] = []; private departmentBudget: number = 0; constructor(private name: string) {} getName(): string { return this.name; } getHeadCount(): number { return this.subUnits.reduce((sum, unit) => sum + unit.getHeadCount(), 0); } getSalaryCost(): number { return this.subUnits.reduce((sum, unit) => sum + unit.getSalaryCost(), 0); } getBudget(): number { // Total budget = own budget + all sub-unit budgets const subBudgets = this.subUnits.reduce( (sum, unit) => sum + unit.getBudget(), 0 ); return this.departmentBudget + subBudgets; } setDepartmentBudget(budget: number): void { this.departmentBudget = budget; } generateReport(): string { const lines: string[] = [ `\n📁 ${this.name}`, ` Headcount: ${this.getHeadCount()}`, ` Salary Cost: $${this.getSalaryCost().toLocaleString()}`, ` Total Budget: $${this.getBudget().toLocaleString()}`, ` Members:` ]; for (const unit of this.subUnits) { lines.push(unit.generateReport()); } return lines.join("\n"); } findByName(name: string): OrganizationalUnit | null { if (this.name.toLowerCase().includes(name.toLowerCase())) { return this; } for (const unit of this.subUnits) { const found = unit.findByName(name); if (found) return found; } return null; } add(unit: OrganizationalUnit): void { this.subUnits.push(unit); } remove(unit: OrganizationalUnit): void { const index = this.subUnits.indexOf(unit); if (index !== -1) { this.subUnits.splice(index, 1); } } getSubUnits(): OrganizationalUnit[] { return [...this.subUnits]; }} // ============================================// Usage// ============================================ // Build organization structureconst company = new Department("TechCorp Inc.");company.setDepartmentBudget(5000000); const engineering = new Department("Engineering");engineering.setDepartmentBudget(2000000); const frontend = new Department("Frontend Team");const backend = new Department("Backend Team"); frontend.add(new Employee("Alice Chen", "Senior Engineer", 150000));frontend.add(new Employee("Bob Smith", "Engineer", 120000));frontend.add(new Employee("Carol White", "Junior Engineer", 85000)); backend.add(new Employee("David Lee", "Tech Lead", 180000));backend.add(new Employee("Eve Johnson", "Senior Engineer", 155000));backend.add(new Employee("Frank Brown", "Engineer", 125000)); engineering.add(frontend);engineering.add(backend); const sales = new Department("Sales");sales.setDepartmentBudget(1000000);sales.add(new Employee("Grace Miller", "Sales Director", 200000));sales.add(new Employee("Henry Wilson", "Account Executive", 95000)); company.add(engineering);company.add(sales); // Uniform operations on entire hierarchyconsole.log(`Total headcount: ${company.getHeadCount()}`); // 8console.log(`Total salary cost: $${company.getSalaryCost().toLocaleString()}`);console.log(`Total budget: $${company.getBudget().toLocaleString()}`);console.log(company.generateReport()); // Find specific unitconst foundUnit = company.findByName("Frontend");if (foundUnit) { console.log(`\nFrontend team headcount: ${foundUnit.getHeadCount()}`);}Application menus are natural composites: menus contain menu items, but some items are submenus (which contain more items). The Composite Pattern enables rendering, keyboard navigation, and action handling uniformly.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
interface MenuComponent { getName(): string; render(depth: number): string; // Keyboard navigation getShortcut(): string | null; isEnabled(): boolean; // Action execute(): void; // Structure add(component: MenuComponent): void; remove(component: MenuComponent): void; getChildren(): MenuComponent[];} // ============================================// Leaf: Menu Item (executable action)// ============================================ class MenuItem implements MenuComponent { private enabled: boolean = true; constructor( private name: string, private shortcut: string | null, private action: () => void ) {} getName(): string { return this.name; } render(depth: number = 0): string { const indent = " ".repeat(depth); const shortcutStr = this.shortcut ? ` (${this.shortcut})` : ""; const disabledStr = this.enabled ? "" : " [disabled]"; return `${indent}▸ ${this.name}${shortcutStr}${disabledStr}`; } getShortcut(): string | null { return this.shortcut; } isEnabled(): boolean { return this.enabled; } setEnabled(enabled: boolean): void { this.enabled = enabled; } execute(): void { if (this.enabled) { this.action(); } } // Menu items can't have children add(component: MenuComponent): void { throw new Error("Cannot add to menu item"); } remove(component: MenuComponent): void { throw new Error("Cannot remove from menu item"); } getChildren(): MenuComponent[] { return []; }} // ============================================// Leaf: Separator// ============================================ class MenuSeparator implements MenuComponent { getName(): string { return "---"; } render(depth: number = 0): string { const indent = " ".repeat(depth); return `${indent}────────────────────`; } getShortcut(): string | null { return null; } isEnabled(): boolean { return false; } execute(): void { /* separators do nothing */ } add(component: MenuComponent): void { throw new Error("Cannot add to separator"); } remove(component: MenuComponent): void { throw new Error("Cannot remove from separator"); } getChildren(): MenuComponent[] { return []; }} // ============================================// Composite: Menu (contains items and submenus)// ============================================ class Menu implements MenuComponent { private children: MenuComponent[] = []; constructor(private name: string) {} getName(): string { return this.name; } render(depth: number = 0): string { const indent = " ".repeat(depth); const lines = [`${indent}📂 ${this.name}`]; for (const child of this.children) { lines.push(child.render(depth + 1)); } return lines.join("\n"); } getShortcut(): string | null { return null; } isEnabled(): boolean { return true; } execute(): void { // Executing a menu opens it (in a real UI) console.log(`Opening menu: ${this.name}`); } add(component: MenuComponent): void { this.children.push(component); } remove(component: MenuComponent): void { const index = this.children.indexOf(component); if (index !== -1) { this.children.splice(index, 1); } } getChildren(): MenuComponent[] { return [...this.children]; } // Find item by shortcut (recursive) findByShortcut(shortcut: string): MenuComponent | null { for (const child of this.children) { if (child.getShortcut() === shortcut) { return child; } // Recurse into submenus const childChildren = child.getChildren(); if (childChildren.length > 0) { const found = (child as Menu).findByShortcut(shortcut); if (found) return found; } } return null; }} // ============================================// Usage: Build an application menu bar// ============================================ const menuBar = new Menu("Menu Bar"); // File menuconst fileMenu = new Menu("File");fileMenu.add(new MenuItem("New", "Ctrl+N", () => console.log("New file")));fileMenu.add(new MenuItem("Open...", "Ctrl+O", () => console.log("Open dialog")));fileMenu.add(new MenuItem("Save", "Ctrl+S", () => console.log("Saving...")));fileMenu.add(new MenuItem("Save As...", "Ctrl+Shift+S", () => console.log("Save as dialog")));fileMenu.add(new MenuSeparator()); // Recent files submenuconst recentMenu = new Menu("Recent Files");recentMenu.add(new MenuItem("document.txt", null, () => console.log("Open document.txt")));recentMenu.add(new MenuItem("notes.md", null, () => console.log("Open notes.md")));fileMenu.add(recentMenu); fileMenu.add(new MenuSeparator());fileMenu.add(new MenuItem("Exit", "Alt+F4", () => console.log("Exiting..."))); // Edit menuconst editMenu = new Menu("Edit");editMenu.add(new MenuItem("Undo", "Ctrl+Z", () => console.log("Undo")));editMenu.add(new MenuItem("Redo", "Ctrl+Y", () => console.log("Redo")));editMenu.add(new MenuSeparator());editMenu.add(new MenuItem("Cut", "Ctrl+X", () => console.log("Cut")));editMenu.add(new MenuItem("Copy", "Ctrl+C", () => console.log("Copy")));editMenu.add(new MenuItem("Paste", "Ctrl+V", () => console.log("Paste"))); menuBar.add(fileMenu);menuBar.add(editMenu); // Render entire menu structureconsole.log(menuBar.render()); // Output:// 📂 Menu Bar// 📂 File// ▸ New (Ctrl+N)// ▸ Open... (Ctrl+O)// ▸ Save (Ctrl+S)// ▸ Save As... (Ctrl+Shift+S)// ────────────────────// 📂 Recent Files// ▸ document.txt// ▸ notes.md// ────────────────────// ▸ Exit (Alt+F4)// 📂 Edit// ▸ Undo (Ctrl+Z)// ... // Handle keyboard shortcutconst item = menuBar.findByShortcut("Ctrl+S");if (item && item.isEnabled()) { item.execute(); // Outputs: "Saving..."}Mathematical expressions naturally form trees: operators combine operands, and operands can themselves be complex expressions. The Composite Pattern enables evaluation, simplification, and transformation of expressions.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
interface Expression { evaluate(): number; toString(): string; simplify(): Expression; // For composite operations getOperands(): Expression[];} // ============================================// Leaf: Number Literal// ============================================ class NumberLiteral implements Expression { constructor(private value: number) {} evaluate(): number { return this.value; } toString(): string { return String(this.value); } simplify(): Expression { return this; // Already simplified } getOperands(): Expression[] { return []; // Literals have no operands }} // ============================================// Composite: Binary Operations// ============================================ class Addition implements Expression { constructor( private left: Expression, private right: Expression ) {} evaluate(): number { return this.left.evaluate() + this.right.evaluate(); } toString(): string { return `(${this.left.toString()} + ${this.right.toString()})`; } simplify(): Expression { const leftSimp = this.left.simplify(); const rightSimp = this.right.simplify(); // If both are literals, compute result if (leftSimp instanceof NumberLiteral && rightSimp instanceof NumberLiteral) { return new NumberLiteral(leftSimp.evaluate() + rightSimp.evaluate()); } // x + 0 = x if (rightSimp instanceof NumberLiteral && rightSimp.evaluate() === 0) { return leftSimp; } // 0 + x = x if (leftSimp instanceof NumberLiteral && leftSimp.evaluate() === 0) { return rightSimp; } return new Addition(leftSimp, rightSimp); } getOperands(): Expression[] { return [this.left, this.right]; }} class Multiplication implements Expression { constructor( private left: Expression, private right: Expression ) {} evaluate(): number { return this.left.evaluate() * this.right.evaluate(); } toString(): string { return `(${this.left.toString()} * ${this.right.toString()})`; } simplify(): Expression { const leftSimp = this.left.simplify(); const rightSimp = this.right.simplify(); // If both are literals, compute result if (leftSimp instanceof NumberLiteral && rightSimp instanceof NumberLiteral) { return new NumberLiteral(leftSimp.evaluate() * rightSimp.evaluate()); } // x * 0 = 0 if ((rightSimp instanceof NumberLiteral && rightSimp.evaluate() === 0) || (leftSimp instanceof NumberLiteral && leftSimp.evaluate() === 0)) { return new NumberLiteral(0); } // x * 1 = x if (rightSimp instanceof NumberLiteral && rightSimp.evaluate() === 1) { return leftSimp; } // 1 * x = x if (leftSimp instanceof NumberLiteral && leftSimp.evaluate() === 1) { return rightSimp; } return new Multiplication(leftSimp, rightSimp); } getOperands(): Expression[] { return [this.left, this.right]; }} class Division implements Expression { constructor( private left: Expression, private right: Expression ) {} evaluate(): number { const rightVal = this.right.evaluate(); if (rightVal === 0) { throw new Error("Division by zero"); } return this.left.evaluate() / rightVal; } toString(): string { return `(${this.left.toString()} / ${this.right.toString()})`; } simplify(): Expression { const leftSimp = this.left.simplify(); const rightSimp = this.right.simplify(); if (leftSimp instanceof NumberLiteral && rightSimp instanceof NumberLiteral) { return new NumberLiteral(leftSimp.evaluate() / rightSimp.evaluate()); } // x / 1 = x if (rightSimp instanceof NumberLiteral && rightSimp.evaluate() === 1) { return leftSimp; } return new Division(leftSimp, rightSimp); } getOperands(): Expression[] { return [this.left, this.right]; }} // ============================================// Usage: Build and evaluate expressions// ============================================ // Expression: (3 + 5) * (10 - 2)const expr = new Multiplication( new Addition(new NumberLiteral(3), new NumberLiteral(5)), new Addition(new NumberLiteral(10), new Multiplication(new NumberLiteral(-1), new NumberLiteral(2)))); console.log(`Expression: ${expr.toString()}`);console.log(`Result: ${expr.evaluate()}`); // Simplification exampleconst withZero = new Addition( new Multiplication(new NumberLiteral(5), new NumberLiteral(0)), new NumberLiteral(7));console.log(`Before simplify: ${withZero.toString()}`);console.log(`After simplify: ${withZero.simplify().toString()}`); // Just "7"The Composite Pattern isn't universally applicable. It's designed for specific situations. Here's how to recognize when Composite is the right choice:
| Scenario | Use Composite? | Reason |
|---|---|---|
| File system (files/folders) | ✅ Yes | Classic part-whole, uniform ops, arbitrary depth |
| UI components | ✅ Yes | Containers hold components, uniform render/event |
| Organization hierarchy | ✅ Yes | Departments contain teams/employees uniformly |
| Shopping cart with products | ❌ No | Cart is container, but products don't contain products |
| Order with line items | ❌ Usually no | Fixed 2-level hierarchy, not recursive |
| Database query builder | ✅ Maybe | Depends on whether queries can nest arbitrarily |
| Simple list of items | ❌ No | No hierarchy needed |
Every pattern involves trade-offs. The Composite Pattern trades some type safety and performance for uniformity and extensibility. This is worthwhile when you have genuine part-whole hierarchies but over-engineering when you don't.
The Composite Pattern frequently appears alongside other patterns, creating powerful combinations:
| Pattern | How It Combines with Composite | Example |
|---|---|---|
| Iterator | Traverses composite tree in various orders | Pre-order, post-order, level-order iteration |
| Visitor | Adds operations without modifying component classes | Code analysis, report generation |
| Decorator | Adds behavior to individual components | Bordered UI component, compressed file |
| Chain of Responsibility | Pass requests up the tree via parent references | Event bubbling in UI frameworks |
| Flyweight | Share common leaf state to reduce memory | Characters in document, particles in graphics |
| Builder | Constructs complex composite trees | Document builder, UI layout builder |
| Prototype | Clone entire composite trees | Copy-paste in graphics editor |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// Visitor pattern adds operations to composite without modifying it interface FileSystemVisitor { visitFile(file: File): void; visitDirectory(dir: Directory): void;} interface FileSystemEntry { accept(visitor: FileSystemVisitor): void; // ... other methods} class File implements FileSystemEntry { accept(visitor: FileSystemVisitor): void { visitor.visitFile(this); }} class Directory implements FileSystemEntry { private children: FileSystemEntry[] = []; accept(visitor: FileSystemVisitor): void { visitor.visitDirectory(this); for (const child of this.children) { child.accept(visitor); // Recurse through tree } }} // Now add operation without touching File/Directoryclass SizeCalculatorVisitor implements FileSystemVisitor { totalSize = 0; visitFile(file: File): void { this.totalSize += file.getSize(); } visitDirectory(dir: Directory): void { // Directories don't have inherent size; children are visited automatically }} // Usageconst root = new Directory("root");// ... add files and folders ... const sizeVisitor = new SizeCalculatorVisitor();root.accept(sizeVisitor);console.log(`Total size: ${sizeVisitor.totalSize}`);We've explored the Composite Pattern comprehensively across this module. Let's consolidate everything we've learned:
| Aspect | Description |
|---|---|
| Intent | Compose objects into tree structures; treat individual and composite objects uniformly |
| Participants | Component, Leaf, Composite |
| Key Structure | Composite contains Component[] (enables recursive composition) |
| Primary Benefit | Uniform treatment; no type-checking in client code |
| Trade-off | Type safety vs transparency in child management |
| Related Patterns | Iterator, Visitor, Decorator, Chain of Responsibility |
You have mastered the Composite Pattern. You understand its motivation, structure, participants, implementation options, and real-world applications. You can recognize when to apply it, implement it correctly, and combine it with other patterns. This knowledge will serve you whenever you encounter hierarchical structures in your software designs.