Lesson 30-Node.js Design Patterns

Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global access point to it. In Node.js, the Singleton pattern is commonly used for scenarios requiring a single global instance, such as database connections or loggers.

class Singleton {
    constructor() {
        if (!Singleton.instance) {
            Singleton.instance = this;
        }
        return Singleton.instance;
    }

    init() {
        console.log('Singleton initialized');
    }
}

// Using the Singleton pattern
const singleton1 = new Singleton();
const singleton2 = new Singleton();

singleton1.init(); // Outputs "Singleton initialized"
singleton2.init(); // Does not output initialization message again

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects, where a change in one object’s state notifies and updates all dependent objects. In Node.js, the event-driven architecture makes the Observer pattern highly practical.

const EventEmitter = require('events');

class Subject extends EventEmitter {}

const subject = new Subject();

subject.on('stateChanged', () => {
    console.log('State has changed');
});

subject.emit('stateChanged'); // Outputs "State has changed"

Factory Pattern

The Factory pattern provides an interface for creating objects, allowing subclasses to decide which class to instantiate. This defers instantiation to subclasses.

class AnimalFactory {
    static create(type) {
        switch (type) {
            case 'dog':
                return new Dog();
            case 'cat':
                return new Cat();
            default:
                throw new Error('Invalid animal type');
        }
    }
}

class Dog {
    speak() {
        console.log('Woof woof');
    }
}

class Cat {
    speak() {
        console.log('Meow meow');
    }
}

const dog = AnimalFactory.create('dog');
const cat = AnimalFactory.create('cat');

dog.speak(); // Outputs "Woof woof"
cat.speak(); // Outputs "Meow meow"

Decorator Pattern

The Decorator pattern allows dynamically adding responsibilities to objects without modifying their structure. In Node.js, it is often used in middleware implementations.

class Logger {
    log(message) {
        console.log(message);
    }
}

function decorateLogger(logger) {
    return {
        log: function (message) {
            logger.log(`[${new Date().toISOString()}] ${message}`);
        },
    };
}

const logger = new Logger();
const decoratedLogger = decorateLogger(logger);

decoratedLogger.log('Hello, World!'); // Outputs log with timestamp

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This allows algorithms to vary independently of the clients using them.

class ShippingStrategy {
    calculateShipping() {}
}

class FlatRateStrategy extends ShippingStrategy {
    calculateShipping(weight) {
        return weight * 5;
    }
}

class FreeShippingStrategy extends ShippingStrategy {
    calculateShipping() {
        return 0;
    }
}

function calculateShipping(weight, strategy) {
    return strategy.calculateShipping(weight);
}

const flatRate = new FlatRateStrategy();
const freeShipping = new FreeShippingStrategy();

console.log(calculateShipping(10, flatRate)); // Outputs 50
console.log(calculateShipping(10, freeShipping)); // Outputs 0

Composite Pattern

The Composite pattern allows composing objects into tree structures to represent part-whole hierarchies, enabling uniform treatment of individual and composite objects.

class Component {
    constructor(name) {
        this.name = name;
    }

    add(component) {
        throw new Error('Method not implemented.');
    }

    remove(component) {
        throw new Error('Method not implemented.');
    }

    display(level) {
        throw new Error('Method not implemented.');
    }
}

class Leaf extends Component {
    display(level) {
        console.log(`${' '.repeat(level)}Leaf: ${this.name}`);
    }
}

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(level) {
        console.log(`${' '.repeat(level)}Composite: ${this.name}`);
        this.children.forEach((child) => child.display(level + 2));
    }
}

const root = new Composite('root');
const branch1 = new Composite('branch1');
const leaf1 = new Leaf('leaf1');
const leaf2 = new Leaf('leaf2');

root.add(branch1);
branch1.add(leaf1);
branch1.add(leaf2);

root.display(0);

Adapter Pattern

The Adapter pattern allows a class’s interface to match another class’s interface, often used to adapt existing libraries to meet application requirements.

Suppose an old library has an incompatible interface; an adapter can bridge the gap.

class OldLibrary {
    oldMethod() {
        return 'Data from old library';
    }
}

class Adapter {
    constructor(oldLibrary) {
        this.oldLibrary = oldLibrary;
    }

    newMethod() {
        return this.oldLibrary.oldMethod();
    }
}

const oldLibrary = new OldLibrary();
const adapter = new Adapter(oldLibrary);

console.log(adapter.newMethod()); // Outputs "Data from old library"

Module Pattern

The Module pattern encapsulates code in JavaScript, implementing private members and exposing a public API. In Node.js, it is used to hide implementation details and expose only necessary interfaces.

const module = (function () {
    let privateData = 'This is private';

    function privateMethod() {
        console.log(privateData);
    }

    return {
        publicMethod: function () {
            privateMethod();
        },
    };
})();

module.publicMethod(); // Outputs "This is private"

Command Pattern

The Command pattern encapsulates a request as an object, enabling parameterization of clients with different requests, queuing or logging requests, and supporting undoable operations.

class Command {
    constructor(receiver) {
        this.receiver = receiver;
    }

    execute() {}
}

class ConcreteCommand extends Command {
    execute() {
        this.receiver.action();
    }
}

class Receiver {
    action() {
        console.log('Action performed');
    }
}

class Invoker {
    constructor() {
        this.command = null;
    }

    setCommand(command) {
        this.command = command;
    }

    invoke() {
        this.command.execute();
    }
}

const receiver = new Receiver();
const command = new ConcreteCommand(receiver);
const invoker = new Invoker();

invoker.setCommand(command);
invoker.invoke(); // Outputs "Action performed"

State Pattern

The State pattern allows an object to change its behavior when its internal state changes, appearing as if it has changed its class.

class Context {
    constructor(state) {
        this.state = state;
    }

    request() {
        this.state.handle();
    }

    setState(state) {
        this.state = state;
    }
}

class State {
    handle() {}
}

class ConcreteStateA extends State {
    handle() {
        console.log('Handling in state A');
    }
}

class ConcreteStateB extends State {
    handle() {
        console.log('Handling in state B');
    }
}

const context = new Context(new ConcreteStateA());
context.request(); // Outputs "Handling in state A"

context.setState(new ConcreteStateB());
context.request(); // Outputs "Handling in state B"

Facade Pattern

The Facade pattern provides a simplified interface to a set of interfaces in a subsystem, defining a higher-level interface to make the subsystem easier to use.

class SubsystemA {
    operationA() {
        console.log('Subsystem A operation');
    }
}

class SubsystemB {
    operationB() {
        console.log('Subsystem B operation');
    }
}

class Facade {
    constructor() {
        this.subsystemA = new SubsystemA();
        this.subsystemB = new SubsystemB();
    }

    performOperation() {
        this.subsystemA.operationA();
        this.subsystemB.operationB();
    }
}

const facade = new Facade();
facade.performOperation(); // Outputs "Subsystem A operation" and "Subsystem B operation"

Proxy Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It is useful when access to an object needs to be restricted due to reasons like access control or high creation costs.

class RealSubject {
    request() {
        console.log('Real subject handling request');
    }
}

class Proxy {
    constructor(realSubject) {
        this.realSubject = realSubject;
    }

    request() {
        if (this.checkAccess()) {
            this.realSubject.request();
        }
    }

    checkAccess() {
        console.log('Checking access');
        return true;
    }
}

const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);

proxy.request(); // Outputs "Checking access" and "Real subject handling request"
Share your love