Structural Patterns Overview
Structural patterns deal with how classes and objects are composed to form larger structures. They help you ensure that when parts of a system change, the entire system doesn't need to change.
Adapter Pattern
Converts the interface of a class into another interface clients expect. It lets classes work together that couldn't otherwise because of incompatible interfaces.
// Old interface โ incompatible with new system
class OldPaymentSystem {
makePayment(amount, currency) {
return `Paid ${amount} ${currency} via old system`;
}
}
// New interface that the system expects
class PaymentProcessor {
processPayment(amount) { throw new Error("Implement me"); }
}
// Adapter โ bridges old and new
class OldPaymentAdapter extends PaymentProcessor {
#oldSystem;
constructor() {
super();
this.#oldSystem = new OldPaymentSystem();
}
processPayment(amount) {
// Adapts new interface to old implementation
return this.#oldSystem.makePayment(amount, "USD");
}
}
// Client code uses the new interface
function checkout(processor, amount) {
console.log(processor.processPayment(amount));
}
// Works with the adapter
checkout(new OldPaymentAdapter(), 99.99);
// Paid 99.99 USD via old system
Decorator Pattern
Attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
// Base component
class Coffee {
getCost() { return 5; }
getDescription() { return "Simple coffee"; }
}
// Base decorator
class CoffeeDecorator {
#coffee;
constructor(coffee) { this.#coffee = coffee; }
getCost() { return this.#coffee.getCost(); }
getDescription() { return this.#coffee.getDescription(); }
}
// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
constructor(coffee) { super(coffee); }
getCost() { return super.getCost() + 1.5; }
getDescription() { return super.getDescription() + ", milk"; }
}
class SugarDecorator extends CoffeeDecorator {
constructor(coffee) { super(coffee); }
getCost() { return super.getCost() + 0.5; }
getDescription() { return super.getDescription() + ", sugar"; }
}
class WhipDecorator extends CoffeeDecorator {
constructor(coffee) { super(coffee); }
getCost() { return super.getCost() + 2; }
getDescription() { return super.getDescription() + ", whip"; }
}
// Stack decorators dynamically
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhipDecorator(coffee);
console.log(coffee.getDescription());
// Simple coffee, milk, sugar, whip
console.log(`$${coffee.getCost()}`);
// $9
Facade Pattern
Provides a simplified interface to a complex subsystem. The facade doesn't add new functionality โ it just makes the subsystem easier to use.
// Complex subsystem classes
class AudioDecoder {
decode(file) { return `Decoded audio: ${file}`; }
}
class VideoDecoder {
decode(file) { return `Decoded video: ${file}`; }
}
class SubtitleParser {
parse(file) { return `Parsed subtitles: ${file}`; }
}
class DisplayRenderer {
render(video, audio, subtitles) {
return `Playing: ${video} with ${audio} and ${subtitles}`;
}
}
class AudioOutput {
play(audio) { return `Audio output: ${audio}`; }
}
// Facade โ simple interface to complex subsystem
class MediaPlayer {
#audioDecoder = new AudioDecoder();
#videoDecoder = new VideoDecoder();
#subtitleParser = new SubtitleParser();
#display = new DisplayRenderer();
#audio = new AudioOutput();
play(videoFile, audioFile, subtitleFile) {
const video = this.#videoDecoder.decode(videoFile);
const audio = this.#audioDecoder.decode(audioFile);
const subtitles = this.#subtitleParser.parse(subtitleFile);
this.#audio.play(audio);
return this.#display.render(video, audio, subtitles);
}
}
// Client uses the simple facade
const player = new MediaPlayer();
console.log(player.play("movie.mp4", "audio.mp3", "subs.srt"));
// Playing: Decoded video: movie.mp4 with Decoded audio: audio.mp3
// and Parsed subtitles: subs.srt
Composite Pattern
Composes objects into tree structures and lets you treat individual objects and compositions uniformly.
// Component
class FileSystemItem {
constructor(name) { this.name = name; }
getSize() { throw new Error("Implement me"); }
display(indent = 0) { throw new Error("Implement me"); }
}
// Leaf
class File extends FileSystemItem {
#size;
constructor(name, size) {
super(name);
this.#size = size;
}
getSize() { return this.#size; }
display(indent = 0) {
console.log(`${" ".repeat(indent)}๐ ${this.name} (${this.#size}KB)`);
}
}
// Composite
class Directory extends FileSystemItem {
#children = [];
constructor(name) { super(name); }
add(item) { this.#children.push(item); return this; }
getSize() {
return this.#children.reduce((sum, child) => sum + child.getSize(), 0);
}
display(indent = 0) {
console.log(`${" ".repeat(indent)}๐ ${this.name}/ (${this.getSize()}KB)`);
this.#children.forEach(child => child.display(indent + 1));
}
}
// Build a tree
const root = new Directory("project");
const src = new Directory("src");
const docs = new Directory("docs");
src.add(new File("index.js", 5));
src.add(new File("app.js", 12));
src.add(new File("utils.js", 8));
docs.add(new File("README.md", 3));
root.add(src);
root.add(docs);
root.add(new File("package.json", 1));
// Treat individual and composite uniformly
root.display();
console.log(`Total size: ${root.getSize()}KB`);
Proxy Pattern
Provides a surrogate or placeholder for another object to control access to it.
// Real image loader
class RealImage {
#filename;
constructor(filename) {
this.#filename = filename;
this.#loadFromDisk();
}
#loadFromDisk() {
console.log(`Loading ${this.#filename} from disk... (slow)`);
}
display() {
console.log(`Displaying ${this.#filename}`);
}
}
// Proxy โ controls access
class ImageProxy {
#filename;
#realImage = null;
constructor(filename) {
this.#filename = filename;
}
display() {
// Lazy loading โ only load when actually needed
if (!this.#realImage) {
this.#realImage = new RealImage(this.#filename);
}
this.#realImage.display();
}
}
// Client doesn't know about the proxy
const image = new ImageProxy("photo.jpg");
console.log("Proxy created (nothing loaded yet)");
image.display(); // Only now does it load from disk
image.display(); // Second call is instant