Classic Design Patterns in JavaScript

Β·

4 min read

Classic Design Patterns in JavaScript

Classic Pattern

  • It's typically around OOP solutions

  • They are categorized in: Creational, Structural, Behavioral

  • In JavaScript (<= ES5) many design patterns are now covered by ES6.

In JavaScript, there are many ways to implement the same design pattern, thanks to the dynamic nature of the language.

1) Creational Design Patterns

  • These patterns aim to solve the problems associated with creating objects in a way that enhances flexibility and reuse of existing code. The primary purpose of creation patterns is to separate the logic of object creation from the rest of the code.

Singleton

  • Problem to Solve: Ensure that a class has ONLY ONE INSTANCE and provides a global point of access to it.

  • Solution: Restrict the instantiation of the class to ONE object and provide a method to access this instance.

  • Use cases

    • Managing a global configuration object.

    • Database connection pooling.

    • UI State Management

    • Logging service.

const DB = {
    connect: async () => {},
    sendQuery: async (query) => {},
};

Factory 🏭

  • Problem to Solve: Object creation can become complex or may involve multiple steps, conditional logic, or dependencies.

  • Solution: The factory pattern encapsulates the object creation process within a separate method or class, isolating it from the rest of the application logic.

  • Use cases

    • UI Element creation

    • Different types of notifications

    • Data parsers


2) Structural Design Patterns πŸ—Ό

  • They are solutions for COMPOSING CLASSES and OBJECTS to form larger structures while keeping them flexible and efficient.

  • They focus on simplifying relationships between entities to ensure the system's maintainability and scalability.

Decorator Pattern πŸŽ„

  • Problem to Solve: Add additional functionality to objects dynamically without modifying their structure.

  • Solution: Wrap the object with another object that adds the desired behavior.

  • Use cases

    • Extending User Interface components with additional features.

    • Adding validation, and caching to method calls.

    • Wrapping API responses to format or process data before passing it on.

Adapter Pattern

  • Problem to Solve: Allow INCOMPATIBLE interfaces to work together.

  • Solution: Create an adapter that translates one interface into another that a client expects.

  • Use cases:

    • Adapting legacy code to work with new systems or APIs

    • Integrating third-party libraries with different interfaces into your application

    • Converting data formats.

Mixins Pattern

  • Problem to Solve: SHARE functionality between classes WITHOUT using inheritance. Because when we change an object's inheritance, we change its nature.

  • Solution: Create a class containing methods that can be used by other classes and apply it to multiple classes.

For example:

let greetingMixin = {
   sayHi () {console.log(`Hi there, my name is ${this.name}`);}
};

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

Object.assign(User.prototype, greetingMixin);

I want to share the sayHi functionality inside the greetingMixin without using inheritance.

We can achieve that behavior with Object.assign , we're injecting every behavior that we have in the greetingMixin object into every object of the User class.

Now we can go ahead, create an instance of User and invoke sayHi directly on the instance.

Additional, by applying the Mixin pattern, we can have the power to injecting behavior into the Class we don't own.

Value Object Pattern

  • Problem to Solve: Represent a value that is immutable, distinct from other objects based on its properties rather than its identity.

  • Solution: Create a class where instances are considered EQUAL if all their properties are equal, and ensure the object is immutable.

  • Use Cases:

    • Representing complex data types like money, dates, or coordinates.

Example:

class Money {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;

        // Freeze the object to make it immutable
        Object.freeze(this);
    };

    equals(other) {
        return other instanceof Money &&
        this.amount === other.amount && 
        this.currentcy === other.currency;
    };
}

3) Behavioral Design Patterns

  • Deal with object INTERACTION and Responsibility Distribution. These patterns focus on HOW objects COMMUNICATE and COOPERATE, ensuring that the system is Flexible and easy to extend.

Observer Pattern (Publisher - Subscriber)

  • Problem to Solve: Allow an object (Subject) to NOTIFY other object (Observers) about CHANGES in ITS STATE without requiring them to be tightly COUPLED.

  • Solution: Define a subject that maintains a list of observers and notifies them of any STATE CHANGES, typically by calling one of their methods.

  • Use Cases:

    • Event handlers.

    • Real-time notifications

    • UI updates

Code example:

class Subject {
    constructor() {
        this.observers = new Set();
    }
    addObserver(obs) {this.observers.add(obs)}
    removeObserver(obs) {this.observers.delete(obs)};

    notifyObservers(message) {
        this.observers.forEach(ob => ob(message));
    }
};

// Usage
subject1.addObserver(message => console.log(`Event fired!`));

Memento Pattern

  • Problem to Solve: Capture and externalize an object's internal state so that it can be restored later, without violating encapsulation.

  • Solution: Create an object that stores the state of the original object and provide methods to save and restore the state

  • Use Cases:

    • Undo / Redo functionality

    • Saving an app session

    • Time-travel debugging

Code example:

class Subject {
    constructor() {
        this.history = [];
    }
    push(state) {this.history.push(createMemento())};

    pop() {
        if (this.history.length === 0) return null;
        return this.history.pop();    
    };
};

Reference Source: https://firt.dev/

Β