Labs ICT
โญ Pro Login

Structural Patterns

Adapter, Decorator, Facade, and Composite patterns for structuring classes.

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

๐Ÿงช Quick Quiz

Which design pattern wraps an object to add new responsibilities dynamically?