Let's be friends

Introduction

These patterns help you design systems that scale well as your projects grow. They focus on organizing relationships between objects, keeping your code modular and maintainable.

Target audience: Developers building larger, more complex projects that require clean, scalable architectures.

In order of practical use

  1. JavaScript

    Adapter pattern

    // Old Monster System (incompatible interface)
    class OldMonster {
      constructor(name) {
        this.name = name;
      }
    
      roar() {
        console.log(`${this.name} roars loudly!`);
      }
    }
    
    // New Monster System (current interface)
    class NewMonster {
      constructor(name) {
        this.name = name;
      }
    
      makeSound() {
        console.log(`${this.name} makes a monster sound!`);
      }
    }
    
    // Adapter to bridge OldMonster and NewMonster
    class MonsterAdapter {
      constructor(oldMonster) {
        this.oldMonster = oldMonster;
      }
    
      makeSound() {
        this.oldMonster.roar(); // Translate the call to the old system
      }
    }
    
    // Example Usage
    const chunky = new OldMonster("Chunky");
    const adaptedChunky = new MonsterAdapter(chunky);
    
    const munchy = new NewMonster("Munchy");
    
    // Using a unified interface
    munchy.makeSound(); // Output: Munchy makes a monster sound!
    adaptedChunky.makeSound(); // Output: Chunky roars loudly!

    How do you handle situations where you need to integrate systems with incompatible interfaces? For example, you might have an old monster system that uses roar() and a new one that uses makeSound(), but you want to use both systems in a consistent way.

    The Adapter Pattern solves this problem by providing a wrapper (adapter) that translates one interface into another. This allows objects with different interfaces to work together seamlessly.

    In this example, MonsterAdapter wraps the OldMonster class and maps its roar() method to the new interface’s makeSound() method. This makes it possible to treat both OldMonster and NewMonster objects the same way.

    This pattern is classified as Structural because it focuses on bridging gaps between systems or components.

  2. JavaScript

    Composite pattern

    // Base Component
    class MonsterComponent {
      constructor(name) {
        this.name = name;
      }
    
      display() {
        throw new Error("This method must be overridden!");
      }
    }
    
    // Leaf Component
    class MonsterLeaf extends MonsterComponent {
      display() {
        console.log(this.name);
      }
    }
    
    // Composite Component
    class MonsterGroup extends MonsterComponent {
      constructor(name) {
        super(name);
        this.children = [];
      }
    
      add(child) {
        this.children.push(child);
      }
    
      remove(child) {
        this.children = this.children.filter((c) => c !== child);
      }
    
      display() {
        console.log(`${this.name}:`);
        this.children.forEach((child) => child.display());
      }
    }
    
    // Example Usage
    const chunky = new MonsterLeaf("Chunky");
    const munchy = new MonsterLeaf("Munchy");
    const crunchy = new MonsterLeaf("Crunchy");
    
    const snackGroup = new MonsterGroup("Snack Monsters");
    snackGroup.add(chunky);
    snackGroup.add(munchy);
    
    const allMonsters = new MonsterGroup("All Monsters");
    allMonsters.add(snackGroup);
    allMonsters.add(crunchy);
    
    allMonsters.display();
    /* Output:
    All Monsters:
    Snack Monsters:
    Chunky
    Munchy
    Crunchy
    */

    How do you organize objects into tree structures where individual objects and groups of objects are treated the same way? For example, you might have monsters grouped by categories (like snack monsters or flying monsters), but you still want to manage them as one unified structure.

    The Composite Pattern solves this problem by treating individual objects (leaves) and groups (composites) uniformly. You can compose objects into tree structures and interact with them as if they were all individual objects.

    In this example, MonsterLeaf represents individual monsters, and MonsterGroup represents groups of monsters. Both share the same interface (display), allowing you to handle them consistently.

    This pattern is classified as Structural because it focuses on organizing and composing objects into flexible structures.

  3. JavaScript

    Facade pattern

    // Subsystems
    class MonsterInventory {
      addMonster(name) {
        console.log(`${name} has been added to the inventory.`);
      }
      removeMonster(name) {
        console.log(`${name} has been removed from the inventory.`);
      }
    }
    
    class MonsterActions {
      feedMonster(name) {
        console.log(`${name} has been fed.`);
      }
      playWithMonster(name) {
        console.log(`${name} is playing.`);
      }
    }
    
    class MonsterHealth {
      checkHealth(name) {
        console.log(`Checking ${name}'s health... All good!`);
      }
    }
    
    // Facade
    class MonsterFacade {
      constructor() {
        this.inventory = new MonsterInventory();
        this.actions = new MonsterActions();
        this.health = new MonsterHealth();
      }
    
      adoptMonster(name) {
        this.inventory.addMonster(name);
        this.health.checkHealth(name);
        console.log(`${name} has been adopted!`);
      }
    
      careForMonster(name) {
        this.actions.feedMonster(name);
        this.actions.playWithMonster(name);
        this.health.checkHealth(name);
        console.log(`${name} is well cared for!`);
      }
    }
    
    // Example Usage
    const monsterManager = new MonsterFacade();
    
    monsterManager.adoptMonster("Chunky");
    // Output:
    // Chunky has been added to the inventory.
    // Checking Chunky's health... All good!
    // Chunky has been adopted!
    
    monsterManager.careForMonster("Chunky");
    // Output:
    // Chunky has been fed.
    // Chunky is playing.
    // Checking Chunky's health... All good!
    // Chunky is well cared for!

    How do you simplify working with a complex system that involves multiple classes or subsystems? For example, managing monsters might involve inventory, actions, and health-checking logic, each handled by separate classes.

    The Facade Pattern solves this problem by providing a unified interface to interact with the system. Instead of calling multiple subsystems directly, you interact with the facade, which handles the coordination behind the scenes.

    In this example, MonsterFacade simplifies working with MonsterInventory, MonsterActions, and MonsterHealth by exposing high-level methods like adoptMonster and careForMonster.

    This pattern is classified as Structural because it focuses on organizing and simplifying interactions within a system.

  4. JavaScript

    Flyweight pattern

    // Flyweight Factory
    class MonsterFactory {
      constructor() {
        this.monsters = {}; // Cache to store shared monster objects
      }
    
      createMonster(type) {
        if (!this.monsters[type]) {
          this.monsters[type] = new Monster(type);
          console.log(`Creating new ${type} monster.`);
        } else {
          console.log(`Reusing existing ${type} monster.`);
        }
        return this.monsters[type];
      }
    }
    
    // Shared Monster Class (intrinsic state)
    class Monster {
      constructor(type) {
        this.type = type;
      }
    
      display(extrinsicData) {
        console.log(
          `${this.type} monster is ${extrinsicData.action} at location: ${extrinsicData.location}.`
        );
      }
    }
    
    // Example Usage
    const factory = new MonsterFactory();
    
    const chunky = factory.createMonster("Chunky");
    chunky.display({ action: "eating cookies", location: "the kitchen" });
    
    const munchy = factory.createMonster("Munchy");
    munchy.display({ action: "playing", location: "the backyard" });
    
    const reusedChunky = factory.createMonster("Chunky");
    reusedChunky.display({ action: "napping", location: "the couch" });
    
    /* Output:
    Creating new Chunky monster.
    Chunky monster is eating cookies at location: the kitchen.
    Creating new Munchy monster.
    Munchy monster is playing at location: the backyard.
    Reusing existing Chunky monster.
    Chunky monster is napping at location: the couch.
    */

    How do you efficiently manage memory when creating many similar objects? For example, if you’re creating thousands of monsters with shared characteristics (like type), it can become wasteful to duplicate the same data for each object.

    The Flyweight Pattern solves this problem by sharing common, intrinsic data (like the monster type) across multiple objects. Only unique, extrinsic data (like the action or location) is stored separately. This reduces memory usage and improves performance.

    In this example, the MonsterFactory ensures that only one instance of each monster type is created. The display method uses extrinsic data (action and location) to customize the shared monster instance.

    This pattern is classified as Structural because it optimizes the structure of objects in memory.

  5. JavaScript

    Bridge pattern

    // Abstraction
    class Monster {
      constructor(name, behavior) {
        this.name = name;
        this.behavior = behavior; // Bridge to the implementation
      }
    
      performAction() {
        console.log(`${this.name} is getting ready...`);
        this.behavior.execute(this.name);
      }
    }
    
    // Implementation
    class FlyBehavior {
      execute(name) {
        console.log(`${name} is flying high in the sky!`);
      }
    }
    
    class SwimBehavior {
      execute(name) {
        console.log(`${name} is swimming gracefully in the water!`);
      }
    }
    
    // Example Usage
    const flyingChunky = new Monster("Chunky", new FlyBehavior());
    const swimmingMunchy = new Monster("Munchy", new SwimBehavior());
    
    flyingChunky.performAction();
    // Output:
    // Chunky is getting ready...
    // Chunky is flying high in the sky!
    
    swimmingMunchy.performAction();
    // Output:
    // Munchy is getting ready...
    // Munchy is swimming gracefully in the water!

    How do you decouple abstraction from implementation so they can evolve independently? For example, you might have different types of monsters (like Chunky or Munchy) with varying abilities (like flying or swimming), but you don’t want the monster types and behaviors tightly coupled.

    The Bridge Pattern solves this by separating the abstraction (e.g., Monster) from the implementation details (e.g., FlyBehavior or SwimBehavior). This allows both to vary independently, making your code more flexible and maintainable.

    In this example, the Monster class uses a behavior implementation to perform actions. You can add new behaviors (like RunBehavior) or new monster types without modifying existing code.

    This pattern is classified as Structural because it organizes how objects and their implementations interact.

Let's be friends