Composite and Bridge Pattern
Two advanced structural patterns for managing complexity. Composite treats individual objects and compositions uniformly (tree structures), while Bridge separates abstraction from implementation to avoid combinatorial explosion of subclasses.
🎯 What You Will Learn
- Composite pattern: part-whole tree hierarchies
- Treating single objects and composites uniformly
- Bridge pattern: separating abstraction from implementation
- Avoiding subclass explosion with Bridge
Composite Pattern
The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It allows clients to treat individual objects and compositions of objects uniformly.
// Component: common interface
interface FileSystemItem {{ '{' }}
getName(): string;
getSize(): number;
print(indent: string): void;
{{ '}' }}
// Leaf: elements without children
class File implements FileSystemItem {{ '{' }}
constructor(
private name: string,
private size: number
) {{ '{' }}{{ '}' }}
getName(): string {{ '{' }}
return this.name;
{{ '}' }}
getSize(): number {{ '{' }}
return this.size;
{{ '}' }}
print(indent: string = ""): void {{ '{' }}
console.log(`${{ '{' }}indent{{ '}' }}📄 ${{ '{' }}this.name{{ '}' }} (${{ '{' }}this.size{{ '}' }}KB)`);
{{ '}' }}
{{ '}' }}
// Composite: elements with children
class Directory implements FileSystemItem {{ '{' }}
private children: FileSystemItem[] = [];
constructor(private name: string) {{ '{' }}{{ '}' }}
getName(): string {{ '{' }}
return this.name;
{{ '}' }}
add(item: FileSystemItem): void {{ '{' }}
this.children.push(item);
{{ '}' }}
remove(item: FileSystemItem): void {{ '{' }}
const index = this.children.indexOf(item);
if (index !== -1) {{ '{' }}
this.children.splice(index, 1);
{{ '}' }}
{{ '}' }}
getSize(): number {{ '{' }}
// Recursive sum of children
return this.children.reduce((total, child) => total + child.getSize(), 0);
{{ '}' }}
print(indent: string = ""): void {{ '{' }}
console.log(`${{ '{' }}indent{{ '}' }}📁 ${{ '{' }}this.name{{ '}' }} (${{ '{' }}this.getSize(){{ '}' }}KB)`);
this.children.forEach(child => child.print(indent + " "));
{{ '}' }}
{{ '}' }}
// Usage: create a tree structure
const root = new Directory("root");
const documents = new Directory("documents");
documents.add(new File("resume.pdf", 120));
documents.add(new File("cover_letter.docx", 45));
const photos = new Directory("photos");
photos.add(new File("vacation.jpg", 2500));
photos.add(new File("family.png", 1800));
const work = new Directory("work");
work.add(new File("project.zip", 15000));
work.add(new File("notes.txt", 5));
documents.add(work); // Nested directory
root.add(documents);
root.add(photos);
root.add(new File("readme.txt", 2));
// Treat everything uniformly
root.print();
// 📁 root (19472KB)
// 📁 documents (15170KB)
// 📄 resume.pdf (120KB)
// 📄 cover_letter.docx (45KB)
// 📁 work (15005KB)
// 📄 project.zip (15000KB)
// 📄 notes.txt (5KB)
// 📁 photos (4300KB)
// 📄 vacation.jpg (2500KB)
// 📄 family.png (1800KB)
// 📄 readme.txt (2KB)
console.log(`Total size: ${{ '{' }}root.getSize(){{ '}' }}KB`); // 19472KB
Composite with Operations
Practical example: menu system with actions:
// Component interface
interface MenuComponent {{ '{' }}
getName(): string;
execute(): void;
print(indent: string): void;
{{ '}' }}
// Leaf: single actions
class MenuItem implements MenuComponent {{ '{' }}
constructor(
private name: string,
private action: () => void
) {{ '{' }}{{ '}' }}
getName(): string {{ '{' }}
return this.name;
{{ '}' }}
execute(): void {{ '{' }}
console.log(`▶️ Executing: ${{ '{' }}this.name{{ '}' }}`);
this.action();
{{ '}' }}
print(indent: string = ""): void {{ '{' }}
console.log(`${{ '{' }}indent{{ '}' }}• ${{ '{' }}this.name{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Composite: submenu
class Menu implements MenuComponent {{ '{' }}
private items: MenuComponent[] = [];
constructor(private name: string) {{ '{' }}{{ '}' }}
getName(): string {{ '{' }}
return this.name;
{{ '}' }}
add(item: MenuComponent): void {{ '{' }}
this.items.push(item);
{{ '}' }}
execute(): void {{ '{' }}
console.log(`📂 Opening menu: ${{ '{' }}this.name{{ '}' }}`);
this.items.forEach(item => item.execute());
{{ '}' }}
print(indent: string = ""): void {{ '{' }}
console.log(`${{ '{' }}indent{{ '}' }}📂 ${{ '{' }}this.name{{ '}' }}`);
this.items.forEach(item => item.print(indent + " "));
{{ '}' }}
{{ '}' }}
// Build hierarchical menu
const mainMenu = new Menu("Main Menu");
const fileMenu = new Menu("File");
fileMenu.add(new MenuItem("New", () => console.log("Creating new file...")));
fileMenu.add(new MenuItem("Open", () => console.log("Opening file...")));
fileMenu.add(new MenuItem("Save", () => console.log("Saving file...")));
fileMenu.add(new MenuItem("Exit", () => console.log("Exiting...")));
const editMenu = new Menu("Edit");
editMenu.add(new MenuItem("Cut", () => console.log("Cutting...")));
editMenu.add(new MenuItem("Copy", () => console.log("Copying...")));
editMenu.add(new MenuItem("Paste", () => console.log("Pasting...")));
const formatMenu = new Menu("Format");
formatMenu.add(new MenuItem("Bold", () => console.log("Applying bold...")));
formatMenu.add(new MenuItem("Italic", () => console.log("Applying italic...")));
editMenu.add(formatMenu); // Nested submenu
mainMenu.add(fileMenu);
mainMenu.add(editMenu);
// Display structure
mainMenu.print();
// 📂 Main Menu
// 📂 File
// • New
// • Open
// • Save
// • Exit
// 📂 Edit
// • Cut
// • Copy
// • Paste
// 📂 Format
// • Bold
// • Italic
// Execute all
fileMenu.execute();
// 📂 Opening menu: File
// ▶️ Executing: New
// Creating new file...
// ▶️ Executing: Open
// Opening file...
// ▶️ Executing: Save
// Saving file...
// ▶️ Executing: Exit
// Exiting...
Composite for UI Components
Realistic example: nested UI components (like React/Angular):
// Component interface
interface UIComponent {{ '{' }}
render(): string;
addClass(className: string): void;
{{ '}' }}
// Leaf: base elements
class Button implements UIComponent {{ '{' }}
private classes: string[] = [];
constructor(private text: string) {{ '{' }}{{ '}' }}
addClass(className: string): void {{ '{' }}
this.classes.push(className);
{{ '}' }}
render(): string {{ '{' }}
const classAttr = this.classes.length > 0 ? ` class="${{ '{' }}this.classes.join(' '){{ '}' }}"` : '';
return `<button${{ '{' }}classAttr{{ '}' }}>${{ '{' }}this.text{{ '}' }}</button>`;
{{ '}' }}
{{ '}' }}
class Input implements UIComponent {{ '{' }}
private classes: string[] = [];
constructor(
private placeholder: string,
private type: string = "text"
) {{ '{' }}{{ '}' }}
addClass(className: string): void {{ '{' }}
this.classes.push(className);
{{ '}' }}
render(): string {{ '{' }}
const classAttr = this.classes.length > 0 ? ` class="${{ '{' }}this.classes.join(' '){{ '}' }}"` : '';
return `<input type="${{ '{' }}this.type{{ '}' }}" placeholder="${{ '{' }}this.placeholder{{ '}' }}"${{ '{' }}classAttr{{ '}' }} />`;
{{ '}' }}
{{ '}' }}
// Composite: container
class Panel implements UIComponent {{ '{' }}
private children: UIComponent[] = [];
private classes: string[] = [];
constructor(private title?: string) {{ '{' }}{{ '}' }}
add(component: UIComponent): void {{ '{' }}
this.children.push(component);
{{ '}' }}
addClass(className: string): void {{ '{' }}
this.classes.push(className);
{{ '}' }}
render(): string {{ '{' }}
const classAttr = this.classes.length > 0 ? ` class="${{ '{' }}this.classes.join(' '){{ '}' }}"` : '';
const titleHtml = this.title ? `<h3>${{ '{' }}this.title{{ '}' }}</h3>` : '';
const childrenHtml = this.children.map(child => child.render()).join('\n ');
return `<div${{ '{' }}classAttr{{ '}' }}>
${{ '{' }}titleHtml{{ '}' }}
${{ '{' }}childrenHtml{{ '}' }}
</div>`;
{{ '}' }}
{{ '}' }}
// Build UI
const form = new Panel("Login Form");
form.addClass("card");
form.addClass("shadow");
const usernameInput = new Input("Enter username");
usernameInput.addClass("form-control");
const passwordInput = new Input("Enter password", "password");
passwordInput.addClass("form-control");
const submitButton = new Button("Login");
submitButton.addClass("btn");
submitButton.addClass("btn-primary");
form.add(usernameInput);
form.add(passwordInput);
form.add(submitButton);
console.log(form.render());
// <div class="card shadow">
// <h3>Login Form</h3>
// <input type="text" placeholder="Enter username" class="form-control" />
// <input type="password" placeholder="Enter password" class="form-control" />
// <button class="btn btn-primary">Login</button>
// </div>
Bridge Pattern
The Bridge pattern separates an abstraction from its implementation, so that both can vary independently. It solves the combinatorial explosion of subclasses when you have two dimensions of variability.
// Implementation: implementation interface
interface Renderer {{ '{' }}
renderCircle(radius: number): void;
renderSquare(side: number): void;
{{ '}' }}
// Concrete Implementations
class VectorRenderer implements Renderer {{ '{' }}
renderCircle(radius: number): void {{ '{' }}
console.log(`🎨 Drawing vector circle with radius ${{ '{' }}radius{{ '}' }}`);
{{ '}' }}
renderSquare(side: number): void {{ '{' }}
console.log(`🎨 Drawing vector square with side ${{ '{' }}side{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class RasterRenderer implements Renderer {{ '{' }}
renderCircle(radius: number): void {{ '{' }}
console.log(`🖼️ Drawing raster circle (pixels) with radius ${{ '{' }}radius{{ '}' }}`);
{{ '}' }}
renderSquare(side: number): void {{ '{' }}
console.log(`🖼️ Drawing raster square (pixels) with side ${{ '{' }}side{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Abstraction: abstract shape
abstract class Shape {{ '{' }}
constructor(protected renderer: Renderer) {{ '{' }}{{ '}' }}
abstract draw(): void;
abstract resize(factor: number): void;
{{ '}' }}
// Refined Abstractions
class Circle extends Shape {{ '{' }}
constructor(renderer: Renderer, private radius: number) {{ '{' }}
super(renderer);
{{ '}' }}
draw(): void {{ '{' }}
this.renderer.renderCircle(this.radius);
{{ '}' }}
resize(factor: number): void {{ '{' }}
this.radius *= factor;
{{ '}' }}
{{ '}' }}
class Square extends Shape {{ '{' }}
constructor(renderer: Renderer, private side: number) {{ '{' }}
super(renderer);
{{ '}' }}
draw(): void {{ '{' }}
this.renderer.renderSquare(this.side);
{{ '}' }}
resize(factor: number): void {{ '{' }}
this.side *= factor;
{{ '}' }}
{{ '}' }}
// Usage: combine abstractions and implementations
const vectorCircle = new Circle(new VectorRenderer(), 5);
vectorCircle.draw(); // 🎨 Drawing vector circle with radius 5
const rasterCircle = new Circle(new RasterRenderer(), 5);
rasterCircle.draw(); // 🖼️ Drawing raster circle (pixels) with radius 5
const vectorSquare = new Square(new VectorRenderer(), 10);
vectorSquare.draw(); // 🎨 Drawing vector square with side 10
const rasterSquare = new Square(new RasterRenderer(), 10);
rasterSquare.draw(); // 🖼️ Drawing raster square (pixels) with side 10
// Without Bridge: you would need VectorCircle, RasterCircle, VectorSquare, RasterSquare
// With 3 shapes and 3 renderers = 9 classes!
// With Bridge: 3 shapes + 3 renderers = 6 classes
Bridge: Avoiding Subclass Explosion
Problem solved by Bridge: two dimensions of variability without combinatorial explosion:
// ❌ Approach with inheritance: class explosion
class SmallRedCircle {{ '{' }}{{ '}' }}
class SmallBlueCircle {{ '{' }}{{ '}' }}
class SmallGreenCircle {{ '{' }}{{ '}' }}
class MediumRedCircle {{ '{' }}{{ '}' }}
class MediumBlueCircle {{ '{' }}{{ '}' }}
class MediumGreenCircle {{ '{' }}{{ '}' }}
class LargeRedCircle {{ '{' }}{{ '}' }}
class LargeBlueCircle {{ '{' }}{{ '}' }}
class LargeGreenCircle {{ '{' }}{{ '}' }}
// 3 sizes x 3 colors = 9 classes just for Circle!
// Add Square, Triangle → 27 classes!
// Add one color → +9 classes!
// Implementation: colors
interface Color {{ '{' }}
fill(): string;
{{ '}' }}
class Red implements Color {{ '{' }}
fill(): string {{ '{' }} return "red"; {{ '}' }}
{{ '}' }}
class Blue implements Color {{ '{' }}
fill(): string {{ '{' }} return "blue"; {{ '}' }}
{{ '}' }}
class Green implements Color {{ '{' }}
fill(): string {{ '{' }} return "green"; {{ '}' }}
{{ '}' }}
// Abstraction: shapes with size
abstract class ColoredShape {{ '{' }}
constructor(protected color: Color) {{ '{' }}{{ '}' }}
abstract draw(): void;
{{ '}' }}
class ColoredCircle extends ColoredShape {{ '{' }}
constructor(color: Color, private size: string) {{ '{' }}
super(color);
{{ '}' }}
draw(): void {{ '{' }}
console.log(`🔴 Drawing ${{ '{' }}this.size{{ '}' }} circle with ${{ '{' }}this.color.fill(){{ '}' }} color`);
{{ '}' }}
{{ '}' }}
class ColoredSquare extends ColoredShape {{ '{' }}
constructor(color: Color, private size: string) {{ '{' }}
super(color);
{{ '}' }}
draw(): void {{ '{' }}
console.log(`🟦 Drawing ${{ '{' }}this.size{{ '}' }} square with ${{ '{' }}this.color.fill(){{ '}' }} color`);
{{ '}' }}
{{ '}' }}
// Usage: combine freely
new ColoredCircle(new Red(), "small").draw();
// 🔴 Drawing small circle with red color
new ColoredCircle(new Blue(), "large").draw();
// 🔴 Drawing large circle with blue color
new ColoredSquare(new Green(), "medium").draw();
// 🟦 Drawing medium square with green color
// 2 shapes + 3 colors = 5 classes (vs 6 with inheritance)
// Add 1 color → +1 class (vs +2)
// Scales linearly!
Bridge for Cross-Platform UI
Practical example: UI that works across different platforms:
// Implementation: platforms
interface Platform {{ '{' }}
showDialog(title: string, message: string): void;
showNotification(message: string): void;
{{ '}' }}
class WindowsPlatform implements Platform {{ '{' }}
showDialog(title: string, message: string): void {{ '{' }}
console.log(`🪟 Windows Dialog: [${{ '{' }}title{{ '}' }}] ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
showNotification(message: string): void {{ '{' }}
console.log(`🪟 Windows Notification: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class MacOSPlatform implements Platform {{ '{' }}
showDialog(title: string, message: string): void {{ '{' }}
console.log(`🍎 macOS Alert: [${{ '{' }}title{{ '}' }}] ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
showNotification(message: string): void {{ '{' }}
console.log(`🍎 macOS Banner: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class LinuxPlatform implements Platform {{ '{' }}
showDialog(title: string, message: string): void {{ '{' }}
console.log(`🐧 Linux Dialog: [${{ '{' }}title{{ '}' }}] ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
showNotification(message: string): void {{ '{' }}
console.log(`🐧 Linux Notify: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Abstraction: application
abstract class Application {{ '{' }}
constructor(protected platform: Platform) {{ '{' }}{{ '}' }}
abstract run(): void;
{{ '}' }}
class MessageApp extends Application {{ '{' }}
run(): void {{ '{' }}
this.platform.showDialog("New Message", "You have 5 new messages");
this.platform.showNotification("Message received!");
{{ '}' }}
{{ '}' }}
class SettingsApp extends Application {{ '{' }}
run(): void {{ '{' }}
this.platform.showDialog("Settings", "Configure your preferences");
{{ '}' }}
{{ '}' }}
// Usage: same app on different platforms
console.log("--- Windows ---");
const windowsMsg = new MessageApp(new WindowsPlatform());
windowsMsg.run();
// 🪟 Windows Dialog: [New Message] You have 5 new messages
// 🪟 Windows Notification: Message received!
console.log("\n--- macOS ---");
const macMsg = new MessageApp(new MacOSPlatform());
macMsg.run();
// 🍎 macOS Alert: [New Message] You have 5 new messages
// 🍎 macOS Banner: Message received!
console.log("\n--- Linux ---");
const linuxSettings = new SettingsApp(new LinuxPlatform());
linuxSettings.run();
// 🐧 Linux Dialog: [Settings] Configure your preferences
When to Use Each Pattern
✅ Use Composite when:
- You want to represent part-whole hierarchies (trees)
- Clients must treat single objects and compositions uniformly
- Recursive structures: file systems, menus, UI components
- Operations must propagate through the hierarchy
✅ Use Bridge when:
- You have two independent dimensions of variability
- You want to avoid combinatorial explosion of subclasses
- Abstraction and implementation must vary independently
- Cross-platform development, rendering engines, database drivers
Composite and Bridge Combined
// BRIDGE: separate document from storage
interface DocumentStorage {{ '{' }}
save(content: string): void;
load(): string;
{{ '}' }}
class LocalFileStorage implements DocumentStorage {{ '{' }}
save(content: string): void {{ '{' }}
console.log(`💾 Saving to local file: ${{ '{' }}content.substring(0, 30){{ '}' }}...`);
{{ '}' }}
load(): string {{ '{' }}
return "Loaded from local file";
{{ '}' }}
{{ '}' }}
class CloudStorage implements DocumentStorage {{ '{' }}
save(content: string): void {{ '{' }}
console.log(`☁️ Uploading to cloud: ${{ '{' }}content.substring(0, 30){{ '}' }}...`);
{{ '}' }}
load(): string {{ '{' }}
return "Loaded from cloud";
{{ '}' }}
{{ '}' }}
// COMPOSITE: document structure
interface DocumentElement {{ '{' }}
render(): string;
{{ '}' }}
class Paragraph implements DocumentElement {{ '{' }}
constructor(private text: string) {{ '{' }}{{ '}' }}
render(): string {{ '{' }}
return `<p>${{ '{' }}this.text{{ '}' }}</p>`;
{{ '}' }}
{{ '}' }}
class Heading implements DocumentElement {{ '{' }}
constructor(
private text: string,
private level: number
) {{ '{' }}{{ '}' }}
render(): string {{ '{' }}
return `<h${{ '{' }}this.level{{ '}' }}>${{ '{' }}this.text{{ '}' }}</h${{ '{' }}this.level{{ '}' }}>`;
{{ '}' }}
{{ '}' }}
class Section implements DocumentElement {{ '{' }}
private elements: DocumentElement[] = [];
add(element: DocumentElement): void {{ '{' }}
this.elements.push(element);
{{ '}' }}
render(): string {{ '{' }}
const content = this.elements.map(e => e.render()).join('\n');
return `<section>\n${{ '{' }}content{{ '}' }}\n</section>`;
{{ '}' }}
{{ '}' }}
// Document with Bridge for storage
class Document {{ '{' }}
private root = new Section();
constructor(private storage: DocumentStorage) {{ '{' }}{{ '}' }}
add(element: DocumentElement): void {{ '{' }}
this.root.add(element);
{{ '}' }}
save(): void {{ '{' }}
const content = this.root.render();
this.storage.save(content);
{{ '}' }}
load(): void {{ '{' }}
const content = this.storage.load();
console.log(`📄 Document loaded: ${{ '{' }}content{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Combined usage
const localDoc = new Document(new LocalFileStorage());
localDoc.add(new Heading("Introduction", 1));
localDoc.add(new Paragraph("This is the introduction paragraph."));
localDoc.add(new Heading("Details", 2));
localDoc.add(new Paragraph("Here are the details."));
localDoc.save();
// 💾 Saving to local file: <section>
// <h1>Introduction</h1>
// <p>Thi...
const cloudDoc = new Document(new CloudStorage());
cloudDoc.add(new Heading("Cloud Document", 1));
cloudDoc.add(new Paragraph("Stored in the cloud."));
cloudDoc.save();
// ☁️ Uploading to cloud: <section>
// <h1>Cloud Document</h1>
// <p>St...
Conclusion
Composite and Bridge solve complex structural problems. Composite unifies single objects and compositions for recursive tree structures, while Bridge separates abstraction from implementation to avoid combinatorial explosions. Use them for scalable and maintainable architectures.
Related deep dive: 02 - Builder e Prototype Pattern: Costruzione Flessibile.
Related deep dive: 03 - Adapter, Decorator e Facade Pattern: Strutture Flessibili.
🎯 Key Takeaways
- Composite = tree hierarchies, treat leaf and composite uniformly
- Use Composite for file systems, menus, UI trees
- Bridge = separate abstraction from implementation
- Bridge avoids N x M subclasses (becomes N + M classes)
- Use Bridge for cross-platform, multi-renderer, multi-database
- Both favor composition over inheritance
Frequently asked questions
What is the main purpose of the Composite pattern in object-oriented design?
The Composite pattern composes objects into tree structures to represent part-whole hierarchies and allows clients to treat individual objects and compositions uniformly.
How does the Bridge pattern address a common problem in software development?
The Bridge pattern solves the combinatorial explosion of subclasses by separating an abstraction from its implementation, allowing both to vary independently.
What is a key benefit of using the Composite pattern in designing user interfaces?
The Composite pattern enables the treatment of individual UI components and compositions uniformly, making it easier to manage complex UI structures.
In what type of software architecture are the Composite and Bridge patterns particularly useful?
They are useful for creating scalable and maintainable architectures.
Commenti
Caricamento commenti...
Sign in to comment