JavaScript Design Patterns: Implementation Guide

JavaScript Design Patterns: Implemantation Guide

Singleton Pattern

The Singleton pattern restricts a class to only one instance while providing a global access point to that instance.

Characteristics:

  1. Ensures only one instance of a class exists
  2. Provides a global access point to that instance
  3. Maintains consistency across multiple calls

Advantages:

  • Ideal for scenarios requiring a single object
  • Reduces memory overhead by avoiding repeated instantiation

Disadvantages:

  • Not suitable for dynamic objects or multiple similar instances

Implementation 1

const DataRepository = (function () {
  function DataRepository(config) {
    this.config = config;
    this.data = [];
  }

  let instance = null;

  return (...args) => {
    if (!instance) {
      instance = new DataRepository(...args);
    }
    return instance;
  }
})();

const repo1 = DataRepository({ type: 'user' });
const repo2 = DataRepository({ type: 'product' });
console.log(repo1);
console.log(repo2);

Implementation 2

let Database = function (connectionString) {
    this.connectionString = connectionString;
    this.connection = null;
};

Database.prototype.connect = function () {
    console.log(`Connecting to database with: ${this.connectionString}`);
    this.connection = "Connected";
};

Database.prototype.isConnected = function () {
    return this.connection !== null;
};

Database.getInstance = function (connectionString) {
    if (!this.instance) {
        this.instance = new Database(connectionString);
    }
    return this.instance;
};

const db1 = Database.getInstance("mysql://localhost:3306/mydb");
const db2 = Database.getInstance("mysql://localhost:3306/otherdb");

console.log(db1);  // Database { connectionString: "mysql://localhost:3306/mydb", connection: null }
console.log(db2);  // Database { connectionString: "mysql://localhost:3306/mydb", connection: null }

db1.connect();  // Connecting to database with: mysql://localhost:3306/mydb
console.log(db1.isConnected());  // true
console.log(db2.isConnected());  // true
console.log(db1 === db2);  // true

Implementation 3

class Configuration {
    // Private static property
    static #instance = null;
    
    // Private constructor
    #settings = {};
    
    constructor() {
        if (Configuration.#instance) {
            return Configuration.#instance;
        }
        this.#settings = {
            theme: 'dark',
            language: 'en'
        };
        Configuration.#instance = this;
    }
    
    getSetting(key) {
        return this.#settings[key];
    }
    
    setSetting(key, value) {
        this.#settings[key] = value;
    }
    
    static getInstance() {
        if (!Configuration.#instance) {
            Configuration.#instance = new Configuration();
        }
        return Configuration.#instance;
    }
}

const config1 = Configuration.getInstance();
const config2 = Configuration.getInstance();

console.log(config1 === config2);  // true

Prototype Pattern

The Prototype pattern creates objects by cloning an existing object rather than creating new instances from scratch.

function Vehicle() {
  this.type = 'land';
}

Vehicle.prototype.getDetails = function() {
  console.log(`Type: ${this.type}, Model: ${this.model}`);
};

const baseVehicle = new Vehicle();
const car = Object.create(baseVehicle);
car.model = 'Sedan';
car.doors = 4;

console.log(baseVehicle); // Vehicle {type: 'land'}
console.log(car); // Vehicle {model: 'Sedan', doors: 4}

baseVehicle.getDetails(); // Type: land, Model: undefined
car.getDetails(); // Type: land, Model: Sedan

const truck = Object.create(baseVehicle);
truck.model = 'Pickup';
truck.capacity = '2 tons';

truck.getDetails(); // Type: land, Model: Pickup

Factory Pattern

The Factory pattern provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.

Simple Factory

function AndroidPhone() {
  this.brand = 'Android';
  this.os = 'Android 12';
}

function iOSPhone() {
  this.brand = 'Apple';
  this.os = 'iOS 15';
}

function PhoneFactory(phoneType) {
  switch(phoneType) {
    case "Android":
      return new AndroidPhone();
    case "iOS":
      return new iOSPhone();
    default:
      throw new Error('Unsupported phone type');
  }
}

const androidPhone = PhoneFactory('Android');
console.log(androidPhone); // {brand: 'Android', os: 'Android 12'}

const iPhone = PhoneFactory('iOS');
console.log(iPhone); // {brand: 'Apple', os: 'iOS 15'}

Factory Method Pattern

function ElectronicsFactory(product) {
  if (this instanceof ElectronicsFactory && this[product]) {
    return new this[product]();
  } else {
    console.log(`No ${product} available in this factory`);
    return null;
  }
}

ElectronicsFactory.prototype.Smartphone = function(){
  this.model = 'Latest Model';
  this.price = 699;
}

ElectronicsFactory.prototype.Laptop = function() {
  this.model = 'Business Pro';
  this.price = 1299;
}

const smartphone = new ElectronicsFactory('Smartphone');
console.log(smartphone); // {model: 'Latest Model', price: 699}

const laptop = new ElectronicsFactory('Laptop');
console.log(laptop); // {model: 'Business Pro', price: 1299}

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

// Validation strategies
const validationStrategies = {
  isEmail: function(value) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(value)) {
      console.log('Invalid email format');
    } else {
      console.log('Email validation passed:', value);
    }
  },
  isNumeric: function(value) {
    if (!/^\d+$/.test(value)) {
      console.log('Value must be numeric');
    } else {
      console.log('Numeric validation passed:', value);
    }
  },
  required: function(value) {
    if (value === '' || value === null || value === undefined) {
      console.log('Value is required');
    } else {
      console.log('Required validation passed:', value);
    }
  }
};

function validateInput(value, rule) {
  if(validationStrategies[rule]) {
    validationStrategies[rule](value);
  }
}

validateInput('test@example.com', 'isEmail');  // Email validation passed: test@example.com
validateInput('123', 'isNumeric');     // Numeric validation passed: 123
validateInput(null, 'required');    // Value is required

Builder Pattern

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

// Product
class House {
  constructor() {
    this.floors = 0;
    this.rooms = 0;
    this.hasGarage = false;
    this.hasSwimmingPool = false;
  }
  
  getInfo() {
    console.log(`House with ${this.floors} floors, ${this.rooms} rooms, ` +
                `Garage: ${this.hasGarage ? 'Yes' : 'No'}, ` +
                `Swimming Pool: ${this.hasSwimmingPool ? 'Yes' : 'No'}`);
  }
}

// Builder
class HouseBuilder {
  constructor() {
    this.house = new House();
  }
  
  addFloors(count) {
    this.house.floors = count;
    return this;
  }
  
  addRooms(count) {
    this.house.rooms = count;
    return this;
  }
  
  addGarage() {
    this.house.hasGarage = true;
    return this;
  }
  
  addSwimmingPool() {
    this.house.hasSwimmingPool = true;
    return this;
  }
  
  build() {
    return this.house;
  }
}

// Usage
const mansion = new HouseBuilder()
  .addFloors(3)
  .addRooms(8)
  .addGarage()
  .addSwimmingPool()
  .build();

const cottage = new HouseBuilder()
  .addFloors(1)
  .addRooms(2)
  .build();

mansion.getInfo(); // House with 3 floors, 8 rooms, Garage: Yes, Swimming Pool: Yes
cottage.getInfo(); // House with 1 floors, 2 rooms, Garage: No, Swimming Pool: No

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

// Subject
class Subject {
  constructor() {
    this.observers = [];
    this.state = null;
  }
  
  setState(state) {
    this.state = state;
    this.notifyAll();
  }
  
  getState() {
    return this.state;
  }
  
  addObserver(observer) {
    this.observers.push(observer);
  }
  
  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
  
  notifyAll() {
    this.observers.forEach(observer => observer.update(this.state));
  }
}

// Observer
class Observer {
  constructor(name) {
    this.name = name;
  }
  
  update(state) {
    console.log(`${this.name} received update. New state: ${state}`);
  }
}

// Usage
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
const observer3 = new Observer('Observer 3');

subject.addObserver(observer1);
subject.addObserver(observer2);
subject.addObserver(observer3);

subject.setState('State 1');
// Observer 1 received update. New state: State 1
// Observer 2 received update. New state: State 1
// Observer 3 received update. New state: State 1

subject.removeObserver(observer2);
subject.setState('State 2');
// Observer 1 received update. New state: State 2
// Observer 3 received update. New state: State 2

Decorator Pattern

The Decorator pattern attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing for extending functionality.

// Base component
class Coffee {
  cost() {
    return 5;
  }
  
  description() {
    return 'Basic coffee';
  }
}

// Decorator
class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  
  cost() {
    return this.coffee.cost();
  }
  
  description() {
    return this.coffee.description();
  }
}

// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 2;
  }
  
  description() {
    return this.coffee.description() + ', with milk';
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 1;
  }
  
  description() {
    return this.coffee.description() + ', with sugar';
  }
}

// Usage
let coffee = new Coffee();
console.log(`${coffee.description()} costs $${coffee.cost()}`);

coffee = new MilkDecorator(coffee);
console.log(`${coffee.description()} costs $${coffee.cost()}`);

coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()} costs $${coffee.cost()}`);

Command Pattern

The Command pattern turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as method arguments, delay or queue a request's execution, and support undoable operations.

// Receiver
class Light {
  turnOn() {
    console.log('Light is ON');
  }
  
  turnOff() {
    console.log('Light is OFF');
  }
}

// Command interface
class Command {
  execute() {}
  undo() {}
}

// Concrete commands
class TurnOnCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }
  
  execute() {
    this.light.turnOn();
  }
  
  undo() {
    this.light.turnOff();
  }
}

class TurnOffCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }
  
  execute() {
    this.light.turnOff();
  }
  
  undo() {
    this.light.turnOn();
  }
}

// Invoker
class RemoteControl {
  constructor() {
    this.commands = [];
    this.undoCommands = [];
  }
  
  setCommand(command) {
    this.commands.push(command);
  }
  
  pressButton() {
    if (this.commands.length > 0) {
      const command = this.commands.pop();
      command.execute();
      this.undoCommands.push(command);
    }
  }
  
  undoButton() {
    if (this.undoCommands.length > 0) {
      const command = this.undoCommands.pop();
      command.undo();
      this.commands.push(command);
    }
  }
}

// Usage
const light = new Light();
const remote = new RemoteControl();

const turnOn = new TurnOnCommand(light);
const turnOff = new TurnOffCommand(light);

remote.setCommand(turnOn);
remote.pressButton(); // Light is ON

remote.setCommand(turnOff);
remote.pressButton(); // Light is OFF

remote.undoButton(); // Light is ON

State Pattern

The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

// Context
class TrafficLight {
  constructor() {
    this.currentState = new RedState(this);
  }
  
  setState(state) {
    this.currentState = state;
  }
  
  requestChange() {
    this.currentState.handleRequest();
  }
}

// State interface
class State {
  constructor(trafficLight) {
    this.trafficLight = trafficLight;
  }
  
  handleRequest() {
    throw new Error("handleRequest() must be implemented");
  }
}

// Concrete states
class RedState extends State {
  handleRequest() {
    console.log('Red light -> Green light');
    this.trafficLight.setState(new GreenState(this.trafficLight));
  }
}

class GreenState extends State {
  handleRequest() {
    console.log('Green light -> Yellow light');
    this.trafficLight.setState(new YellowState(this.trafficLight));
  }
}

class YellowState extends State {
  handleRequest() {
    console.log('Yellow light -> Red light');
    this.trafficLight.setState(new RedState(this.trafficLight));
  }
}

// Usage
const trafficLight = new TrafficLight();
trafficLight.requestChange(); // Red light -> Green light
trafficLight.requestChange(); // Green light -> Yellow light
trafficLight.requestChange(); // Yellow light -> Red light

Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.

// Legacy system with incompatible interface
class OldSystem {
  calculate(data) {
    console.log('Processing with old system:', data);
    return data * 2;
  }
}

// New system interface
class NewSystem {
  process(data) {
    // Implementation expected by client
  }
}

// Adapter
class SystemAdapter extends NewSystem {
  constructor(oldSystem) {
    super();
    this.oldSystem = oldSystem;
  }
  
  process(data) {
    return this.oldSystem.calculate(data);
  }
}

// Usage
const oldSystem = new OldSystem();
const adaptedSystem = new SystemAdapter(oldSystem);

const result = adaptedSystem.process(10);
console.log('Result:', result);

Facade Pattern

The Facade pattern provides a simplified interface to a complex subsystem, making it easier to use.

// Complex subsystem
class AudioSystem {
  turnOn() {
    console.log('Audio system turned on');
  }
  
  setVolume(level) {
    console.log(`Audio volume set to ${level}%`);
  }
}

class VideoSystem {
  turnOn() {
    console.log('Video system turned on');
  }
  
  setResolution(resolution) {
    console.log(`Video resolution set to ${resolution}`);
  }
}

class NetworkSystem {
  connect() {
    console.log('Network connected');
  }
  
  streamContent() {
    console.log('Content streaming started');
  }
}

// Facade
class HomeTheaterFacade {
  constructor() {
    this.audio = new AudioSystem();
    this.video = new VideoSystem();
    this.network = new NetworkSystem();
  }
  
  watchMovie() {
    console.log('Getting ready to watch a movie...');
    this.audio.turnOn();
    this.audio.setVolume(80);
    this.video.turnOn();
    this.video.setResolution('4K');
    this.network.connect();
    this.network.streamContent();
  }
  
  listenToMusic() {
    console.log('Getting ready to listen to music...');
    this.audio.turnOn();
    this.audio.setVolume(50);
    this.network.connect();
    this.network.streamContent();
  }
}

// Usage
const homeTheater = new HomeTheaterFacade();
homeTheater.watchMovie();
// Getting ready to watch a movie...
// Audio system turned on
// Audio volume set to 80%
// Video system turned on
// Video resolution set to 4K
// Network connected
// Content streaming started

homeTheater.listenToMusic();
// Getting ready to listen to music...
// Audio system turned on
// Audio volume set to 50%
// Network connected
// Content streaming started

Proxy Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access to it.

// Real subject
class RealImage {
  constructor(filename) {
    this.filename = filename;
    this.loadImage();
  }
  
  loadImage() {
    console.log(`Loading image: ${this.filename}`);
  }
  
  display() {
    console.log(`Displaying image: ${this.filename}`);
  }
}

// Proxy
class ProxyImage {
  constructor(filename) {
    this.filename = filename;
    this.realImage = null;
  }
  
  display() {
    if (!this.realImage) {
      this.realImage = new RealImage(this.filename);
    }
    this.realImage.display();
  }
}

// Usage
const image1 = new ProxyImage("photo1.jpg");
const image2 = new ProxyImage("photo2.jpg");

image1.display(); // Loads and displays the image
image1.display(); // Just displays the image (already loaded)
image2.display(); // Loads and displays another image

Chain of Responsibility Pattern

The Chain of Responsibility pattern passes the request along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.

// Handler interface
class Approver {
  setNext(approver) {
    this.next = approver;
  }
  
  processRequest(request) {
    if (this.next) {
      this.next.processRequest(request);
    }
  }
}

// Concrete handlers
class TeamLead extends Approver {
  processRequest(request) {
    if (request.amount <= 500) {
      console.log(`TeamLead approved request for $${request.amount}`);
    } else if (this.next) {
      this.next.processRequest(request);
    } else {
      console.log('No one can approve this request');
    }
  }
}

class Manager extends Approver {
  processRequest(request) {
    if (request.amount <= 2000) {
      console.log(`Manager approved request for $${request.amount}`);
    } else if (this.next) {
      this.next.processRequest(request);
    } else {
      console.log('No one can approve this request');
    }
  }
}

class Director extends Approver {
  processRequest(request) {
    if (request.amount <= 10000) {
      console.log(`Director approved request for $${request.amount}`);
    } else {
      console.log('Request exceeds maximum approval limit');
    }
  }
}

// Request class
class PurchaseRequest {
  constructor(amount, purpose) {
    this.amount = amount;
    this.purpose = purpose;
  }
}

// Usage
const teamLead = new TeamLead();
const manager = new Manager();
const director = new Director();

teamLead.setNext(manager);
manager.setNext(director);

const request1 = new PurchaseRequest(300, 'Office supplies');
const request2 = new PurchaseRequest(1500, 'Software license');
const request3 = new PurchaseRequest(8000, 'New equipment');
const request4 = new PurchaseRequest(15000, 'Company car');

teamLead.processRequest(request1); // TeamLead approved request for $300
teamLead.processRequest(request2); // Manager approved request for $1500
teamLead.processRequest(request3); // Director approved request for $8000
teamLead.processRequest(request4); // Request exceeds maximum approval limit

Template Method Pattern

The Template Method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

// Abstract class
class DataProcessor {
  process(data) {
    this.validate(data);
    this.transform(data);
    this.save(data);
  }
  
  validate(data) {
    throw new Error('validate() must be implemented');
  }
  
  transform(data) {
    throw new Error('transform() must be implemented');
  }
  
  save(data) {
    throw new Error('save() must be implemented');
  }
}

// Concrete classes
class JSONDataProcessor extends DataProcessor {
  validate(data) {
    if (typeof data !== 'object' || data === null) {
      throw new Error('Invalid JSON data');
    }
    console.log('JSON data validated');
  }
  
  transform(data) {
    const transformed = JSON.stringify(data, null, 2);
    console.log('JSON data transformed');
    return transformed;
  }
  
  save(data) {
    console.log('JSON data saved to file');
    return true;
  }
}

class XMLDataProcessor extends DataProcessor {
  validate(data) {
    if (!data.includes('<root>')) {
      throw new Error('Invalid XML data');
    }
    console.log('XML data validated');
  }
  
  transform(data) {
    const transformed = `<root>${data}</root>`;
    console.log('XML data transformed');
    return transformed;
  }
  
  save(data) {
    console.log('XML data saved to file');
    return true;
  }
}

// Usage
const jsonProcessor = new JSONDataProcessor();
const xmlProcessor = new XMLDataProcessor();

const jsonData = { name: 'John', age: 30 };
const xmlData = '<person>John</person>';

jsonProcessor.process(jsonData);
// JSON data validated
// JSON data transformed
// JSON data saved to file

xmlProcessor.process(xmlData);
// XML data validated
// XML data transformed
// XML data saved to file

Visitor Pattern

The Visitor pattern represents an operation to be performed on elements of an object structure. It lets you define a new operation without changing the classes of the elements on which it operates.

// Element interface
class Element {
  accept(visitor) {
    throw new Error('accept() must be implemented');
  }
}

// Concrete elements
class Book extends Element {
  constructor(price) {
    super();
    this.price = price;
  }
  
  accept(visitor) {
    return visitor.visitBook(this);
  }
}

class Fruit extends Element {
  constructor(price, weight) {
    super();
    this.price = price;
    this.weight = weight;
  }
  
  accept(visitor) {
    return visitor.visitFruit(this);
  }
}

// Visitor interface
class Visitor {
  visitBook(book) {
    throw new Error('visitBook() must be implemented');
  }
  
  visitFruit(fruit) {
    throw new Error('visitFruit() must be implemented');
  }
}

// Concrete visitors
class PriceVisitor extends Visitor {
  visitBook(book) {
    return book.price;
  }
  
  visitFruit(fruit) {
    return fruit.price * fruit.weight;
  }
}

class TaxVisitor extends Visitor {
  visitBook(book) {
    return book.price * 1.08; // 8% tax for books
  }
  
  visitFruit(fruit) {
    return fruit.price * fruit.weight * 1.05; // 5% tax for fruits
  }
}

// Usage
const items = [
  new Book(20),
  new Fruit(2, 3), // $2 per kg, 3kg
  new Fruit(5, 1)  // $5 per kg, 1kg
];

const priceVisitor = new PriceVisitor();
const taxVisitor = new TaxVisitor();

let totalPrice = 0;
let totalTax = 0;

for (const item of items) {
  totalPrice += item.accept(priceVisitor);
  totalTax += item.accept(taxVisitor);
}

console.log(`Total price: $${totalPrice}`); // Total price: $31
console.log(`Total tax: $${totalTax}`);     // Total tax: $33.4

Memento Pattern

The Memento pattern captures and externalizes an object's internal state sothat the object can be restored to this state later, without violating encapsulation.

// Memento
class Memento {
  constructor(state) {
    this.state = state;
  }
  
  getState() {
    return this.state;
  }
}

// Originator
class TextEditor {
  constructor() {
    this.content = '';
  }
  
  type(text) {
    this.content += text;
  }
  
  getContent() {
    return this.content;
  }
  
  save() {
    return new Memento(this.content);
  }
  
  restore(memento) {
    this.content = memento.getState();
  }
}

// Caretaker
class History {
  constructor() {
    this.mementos = [];
  }
  
  save(memento) {
    this.mementos.push(memento);
  }
  
  get(index) {
    return this.mementos[index];
  }
}

// Usage
const editor = new TextEditor();
const history = new History();

editor.type('This is the first sentence. ');
history.save(editor.save());

editor.type('This is the second sentence. ');
history.save(editor.save());

editor.type('This is the third sentence.');

console.log(editor.getContent()); // This is the first sentence. This is the second sentence. This is the third sentence.

editor.restore(history.get(1));
console.log(editor.getContent()); // This is the first sentence. This is the second sentence.

editor.restore(history.get(0));
console.log(editor.getContent()); // This is the first sentence.

Mediator Pattern

The Mediator pattern defines an object that centralizes communications between a set of other objects. It promotes loose coupling by keeping objects from referring to each other explicitly.

// Colleague interface
class Colleague {
  constructor(mediator) {
    this.mediator = mediator;
  }
  
  send(message) {
    this.mediator.send(message, this);
  }
  
  receive(message) {
    throw new Error('receive() must be implemented');
  }
}

// Concrete colleagues
class User extends Colleague {
  constructor(name, mediator) {
    super(mediator);
    this.name = name;
  }
  
  receive(message) {
    console.log(`${this.name} received: ${message}`);
  }
  
  sendMessage(message) {
    console.log(`${this.name} sending: ${message}`);
    this.send(message);
  }
}

// Mediator interface
class ChatMediator {
  send(message, sender) {
    throw new Error('send() must be implemented');
  }
}

// Concrete mediator
class ChatRoom extends ChatMediator {
  constructor() {
    super();
    this.users = [];
  }
  
  addUser(user) {
    this.users.push(user);
  }
  
  send(message, sender) {
    for (const user of this.users) {
      if (user !== sender) {
        user.receive(message);
      }
    }
  }
}

// Usage
const chatRoom = new ChatRoom();
const john = new User('John', chatRoom);
const jane = new User('Jane', chatRoom);
const bob = new User('Bob', chatRoom);

chatRoom.addUser(john);
chatRoom.addUser(jane);
chatRoom.addUser(bob);

john.sendMessage('Hello everyone!');
// John sending: Hello everyone!
// Jane received: Hello everyone!
// Bob received: Hello everyone!

jane.sendMessage('Hi John!');
// Jane sending: Hi John!
// John received: Hi John!
// Bob received: Hi John!

Composite Pattern

The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.

// Component interface
class Component {
  constructor(name) {
    this.name = name;
  }
  
  add(component) {
    throw new Error('add() must be implemented');
  }
  
  remove(component) {
    throw new Error('remove() must be implemented');
  }
  
  display(depth) {
    throw new Error('display() must be implemented');
  }
}

// Leaf
class Leaf extends Component {
  constructor(name) {
    super(name);
  }
  
  add(component) {
    console.log('Cannot add to a leaf');
  }
  
  remove(component) {
    console.log('Cannot remove from a leaf');
  }
  
  display(depth) {
    console.log(`${' -'.repeat(depth)}${this.name}`);
  }
}

// Composite
class Composite extends Component {
  constructor(name) {
    super(name);
    this.children = [];
  }
  
  add(component) {
    this.children.push(component);
  }
  
  remove(component) {
    const index = this.children.indexOf(component);
    if (index !== -1) {
      this.children.splice(index, 1);
    }
  }
  
  display(depth) {
    console.log(`${' -'.repeat(depth)}${this.name}`);
    for (const child of this.children) {
      child.display(depth + 1);
    }
  }
}

// Usage
const tree = new Composite('Root');
const branch1 = new Composite('Branch 1');
const branch2 = new Composite('Branch 2');
const leaf1 = new Leaf('Leaf 1');
const leaf2 = new Leaf('Leaf 2');
const leaf3 = new Leaf('Leaf 3');

tree.add(branch1);
tree.add(branch2);
branch1.add(leaf1);
branch1.add(leaf2);
branch2.add(leaf3);

tree.display(0);
// Root
//  -Branch 1
//   -Leaf 1
//   -Leaf 2
//  -Branch 2
//   -Leaf 3

Flyweight Pattern

The Flyweight pattern minimizes memory usage by sharing as much data as possible with similar objects.

// Flyweight factory
class FlyweightFactory {
  constructor() {
    this.flyweights = {};
  }
  
  getFlyweight(sharedState) {
    if (!this.flyweights[sharedState]) {
      this.flyweights[sharedState] = new Flyweight(sharedState);
      console.log(`Creating new flyweight for: ${sharedState}`);
    }
    return this.flyweights[sharedState];
  }
  
  getFlyweightCount() {
    return Object.keys(this.flyweights).length;
  }
}

// Flyweight
class Flyweight {
  constructor(sharedState) {
    this.sharedState = sharedState;
  }
  
  operation(uniqueState) {
    console.log(`Flyweight: ${this.sharedState}, Unique: ${uniqueState}`);
  }
}

// Context
class Context {
  constructor(flyweight, uniqueState) {
    this.flyweight = flyweight;
    this.uniqueState = uniqueState;
  }
  
  operation() {
    this.flyweight.operation(this.uniqueState);
  }
}

// Usage
const factory = new FlyweightFactory();
const flyweight1 = factory.getFlyweight('State A');
const flyweight2 = factory.getFlyweight('State B');
const flyweight3 = factory.getFlyweight('State A'); // Reused

const context1 = new Context(flyweight1, 'Context 1');
const context2 = new Context(flyweight2, 'Context 2');
const context3 = new Context(flyweight3, 'Context 3');

context1.operation(); // Flyweight: State A, Unique: Context 1
context2.operation(); // Flyweight: State B, Unique: Context 2
context3.operation(); // Flyweight: State A, Unique: Context 3

console.log(`Flyweight count: ${factory.getFlyweightCount()}`); // Flyweight count: 2

Iterator Pattern

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

// Iterator interface
class Iterator {
  next() {
    throw new Error('next() must be implemented');
  }
  
  hasNext() {
    throw new Error('hasNext() must be implemented');
  }
}

// Concrete iterator
class ArrayIterator extends Iterator {
  constructor(array) {
    super();
    this.array = array;
    this.position = 0;
  }
  
  next() {
    if (this.hasNext()) {
      return this.array[this.position++];
    }
    return null;
  }
  
  hasNext() {
    return this.position < this.array.length;
  }
}

// Aggregate interface
class Aggregate {
  iterator() {
    throw new Error('iterator() must be implemented');
  }
}

// Concrete aggregate
class NumbersCollection extends Aggregate {
  constructor(numbers) {
    super();
    this.numbers = numbers;
  }
  
  iterator() {
    return new ArrayIterator(this.numbers);
  }
}

// Usage
const collection = new NumbersCollection([1, 2, 3, 4, 5]);
const iterator = collection.iterator();

while (iterator.hasNext()) {
  console.log(iterator.next()); // 1, 2, 3, 4, 5
}

Bridge Pattern

The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently.

// Implementation interface
class DrawingAPI {
  drawCircle(x, y, radius) {
    throw new Error('drawCircle() must be implemented');
  }
  
  drawLine(x1, y1, x2, y2) {
    throw new Error('drawLine() must be implemented');
  }
}

// Concrete implementations
class SVGDrawingAPI extends DrawingAPI {
  drawCircle(x, y, radius) {
    console.log(`<circle cx="${x}" cy="${y}" r="${radius}" />`);
  }
  
  drawLine(x1, y1, x2, y2) {
    console.log(`<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />`);
  }
}

class CanvasDrawingAPI extends DrawingAPI {
  drawCircle(x, y, radius) {
    console.log(`Drawing circle at (${x}, ${y}) with radius ${radius}`);
  }
  
  drawLine(x1, y1, x2, y2) {
    console.log(`Drawing line from (${x1}, ${y1}) to (${x2}, ${y2})`);
  }
}

// Abstraction
class Shape {
  constructor(drawingAPI) {
    this.drawingAPI = drawingAPI;
  }
  
  draw() {
    throw new Error('draw() must be implemented');
  }
  
  resizeByPercentage(percentage) {
    throw new Error('resizeByPercentage() must be implemented');
  }
}

// Refined abstractions
class Circle extends Shape {
  constructor(x, y, radius, drawingAPI) {
    super(drawingAPI);
    this.x = x;
    this.y = y;
    this.radius = radius;
  }
  
  draw() {
    this.drawingAPI.drawCircle(this.x, this.y, this.radius);
  }
  
  resizeByPercentage(percentage) {
    this.radius *= percentage / 100;
  }
}

class Rectangle extends Shape {
  constructor(x, y, width, height, drawingAPI) {
    super(drawingAPI);
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
  }
  
  draw() {
    this.drawingAPI.drawLine(this.x, this.y, this.x + this.width, this.y);
    this.drawingAPI.drawLine(this.x + this.width, this.y, this.x + this.width, this.y + this.height);
    this.drawingAPI.drawLine(this.x + this.width, this.y + this.height, this.x, this.y + this.height);
    this.drawingAPI.drawLine(this.x, this.y + this.height, this.x, this.y);
  }
  
  resizeByPercentage(percentage) {
    this.width *= percentage / 100;
    this.height *= percentage / 100;
  }
}

// Usage
const svgAPI = new SVGDrawingAPI();
const canvasAPI = new CanvasDrawingAPI();

const circle = new Circle(10, 10, 5, svgAPI);
circle.draw(); // <circle cx="10" cy="10" r="5" />

const rectangle = new Rectangle(20, 20, 10, 5, canvasAPI);
rectangle.draw(); // Drawing line from (20, 20) to (30, 20)...

circle.resizeByPercentage(200);
circle.draw(); // <circle cx="10" cy="10" r="10" />

Tags: javascript Design Patterns Software Architecture Object-Oriented Programming

Posted on Sun, 10 May 2026 13:29:48 +0000 by Blue Blood