Hi, I am Sanjeet Tiwari...
Let's talk about

Back to Notes

Design Patterns

JavaScript

These are some of the ways a code can be written to minimize bugs and improve the quality and the readability of the code.

Mainly, 3 types of Design Patterns are there -

  1. Creational
  2. Structural
  3. Behavioral

Creational Patterns

Singleton

Only one instance of the storage class is created, which is being used by everyone in the application.

In the context of JavaScript, this can be achieved simply by having a global object.

const all = {
    name: "Sanjeet",
    age: 27
}

Prototype

It’s another way of creating clones in our application code. Multiple objects which have got similar properties shared via a prototype.

Prototypal Inheritance is a good example of this, wherein new objects are created via Object.create function which takes in a prototype object.

const zombie = {
    eatBrains() {
        console.log("Eating brains...");
    }
}

const chad = Object.create(zombie, { name: "Chad" })

Now, if print out chad alone - console.log(chad), we won’t be able to see eatBrains() function there, but can be accessed via Object.getPrototypeOf() function.

JavaScript, when not able to find something in an object, goes recursively through the prototype chain to find the requested property. Hence, chad.eatBrains() will work smoothly.

Factory

This simply states that, instead of you creating new objects, the responsibility of creating new objects lies in the hands of a factory function, which does the necessary computation and logic to obtain the right kind of object.

One example of this can be -

const iosB = new IOSButton();
const andB = new AndroidButton();

const button = (platform === "ios") ? iosB : andB;

Instead of this, we can have a factory function which determines which platform is it, and then returns the correct object back -

const factory = {
    createButton() {
        if (platform === "ios") return new IOSButton();
        return new AndroidButton();
    }
}

const button = factory.createButton();

Builder

Usually, its the constructor’s job to assign initial values to the variables/properties of a class, and usually looks like -

class Bike {
    constructor(makeYear, model, kms, insured) {
        this.makeYear = makeYear;
        this.model = model;
        this.kms = kms;
        this.insured = insured;
    }
}

const apache = new Bike('2007', 'Apache RTR 160', 18000, true);

Here while creating the new object apache, we are passing direct values to the constructor, which can lead to a bit of confusion as to which property is getting which value.

A way we can fix this -

class Bike {
    constructor(model) {
        this.model = model;
    }
    addKms(kms) {
        this.kms = kms;
        return this;
    }
    addMakeYear(makeYear) {
        this.makeYear = makeYear;
        return this;
    }
    isInsured(insured) {
        this.insured = insured;
        return this;
    }
}

const apache = new Bike("Apache RTR 160").addKms(18000).addMakeYear("2017").isInsured(true);

In a builder class, all methods which assign some value to the properties, the same instance is returned from the method.

Structural Patterns

Facade

This pattern also relates to the D of S.O.L.I.D principles which is Dependency Inversion.

The user does not need to know the internal complexities and low-level code of the plugins and dependencies used, instead, a facade can be placed in the middle of your high-level code, and the low-level code of your dependencies so that your high-level code doesn’t change whenever the logic or complexities in the dependencies change.

Example -

class PlumbingSystem {
    turnOn() {
        // low-level code which handles the complex
        // operation
    }
    turnOff() {
        // ...
    }
}

class ElectricalSystem {
    turnOn() {
        // low-level code which handles the complex
        // operation
    }
    turnOff() {
        // ...
    }
}

class House {
    private plumbing = new PlumbingSystem();
    private electrical = new ElectricalSystem();

    turnOnSystems() {
        plumbing.turnOn();
        electrical.turnOn();
    }
}

const house = new House();

here the user is just concerned with turning the systems ON in his/her house, which can be done simply by calling house.turnOnSystems().

Proxy

A Proxy, as the name suggests, is nothing but a substitute for the real thing. Proxies sits at front of the targets, and act as one, so that some extra validations and checks can be performed on it, before updating the real target.

In JavaScript, this is possible via Proxy constructor, which takes in the original object and the handler -

const original = {
    name: "Sanjeet"
}
const proxy = new Proxy(original, {
    get(target, key) {
        // do anything with the target[key]
        // then get it
        return target[key]
    }
    set(target, key, value) {
        // ... do whatever needs to happen before setting the
        // actual target
        return Reflect.set(target, key, value);
    }
})

Reflect is a special operation, which updates the target with the provided value.

Now, values can be accessed and updated via proxy object - proxy.name.

Composite

Composite design pattern is a way of organizing objects. It helps us handle different objects in a similar way when they are put together to create a structure with parts or wholes. It helps us organize different objects that cumulatively resembles a tree-like structure.

There are 3 main elements of Composite design pattern -

An example of this would be a directory structure -

class Component {
    print() {
        // to be implemented
    }
    size() {
        // to be implemented
    }
}

We have defined a Component which relates all parts and wholes of a directory structure namely - Files and Folders.

In this case a File is a Leaf node -

class File extends Component {
    constructor(name, size) {
        super();
        this.name = name;
        this.size = size;
    }
    print() {
        console.log(`File name: ${this.name}; File size: ${this.size}`);
    }
    size() {
        return this.size;
    }
}

And a Folder is a Composite node -

class Folder {
    constructor(files = [], name) {
        super();
        this.files = files;
        this.name = name;
    }
    print() {
        console.log(`Folder Name: ${this.name}`);
    }
    size() {
        let size = 0;
        for (let file of this.files) {
            size += file.size();
        }
        return size;
    }
    addAFile(file) {
        this.files.push(file);
    }
}

In this way, it becomes very much easy to add new elements to the mix, whether it be a Composite or a Leaf node.

Decorator

A decorator patterns enables an object to have added behaviors on top of the existing concrete behavior. It also has got some elements -

A good example of this will be how Pizza works. The component of a basic Pizza class will look something like this -

class Pizza {
    constructor(name, toppings = [], crust) {
        this.name = name;
        this.toppings = toppings;
        this.crust = crust;
    }
    bake() {
        console.log(`Baking ${this.name} pizza`);
    }
}

The Concrete class of Pizza which is nothing but the most basic Pizza will look something like -

class APizza extends Pizza{
    constructor(name, toppings, crust) {
        super(name, toppings, crust);
    }
}

This is the most basic implementation of Pizza class.

Then we will have a decorator which will specify how different objects can exhibit different behaviors on top of the basic one.

class PizzaDecorator extends Pizza {
    constructor(pizza) {
        super(pizza.name, pizza.toppings, pizza.crust)
    }

    bake() {
        this.pizza.bake();
    }
}

And now, finally we can have Concrete Decorators which are the actual objects which add to the functionality.

class PizzaWithCheese extends PizzaDecorator {
    constructor(pizza, cheeseInGrams) {
        super(pizza);
        this.cheeseInGrams = cheeseInGrams;
    }

    bake() {
        super.bake();
        console.log(`Adding ${cheeseInGrams}g cheese on top`);
    }
}

And that’s how other variations can be created.

Behavioral Patterns

Iterator

This pattern is needed whenever a collection of data needs to be traversed. JavaScript has provided a utility Iterator for this purpose.

Iterators are entities in JavaScript, which has got a next() function which provides the next value, and whether the traversal is done or not in an object - { done: true/false, value: <val> }.

for(..of..) loop can only be run on Iterators, or iterable collections.

One example of it would be a range function in JavaScript which we don’t get out of the box.

const range = (start, stop, step = 1) => {
    let s = start;
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (s < stop) {
                const objectToReturn = { done: false, value: s }
                s = s + step;
                return objectToReturn;
            }
            return { done: true, value: s }
        }
    }
}

for(let n of range(1, 5, 1)) {
    console.log(n) // This will print 1, 2, 3, 4
}

The symbol iterator method -

[Symbol.iterator]() {
    return this;
}

lets us use the range() function inside for(..of..) loop.

Observer

In this pattern, everyone is an observer and listens onto a common signal.

This is like a TV broadcast, where everyone is tuned to the same signal, and are able to view the same data worldwide.

Observer pattern represents One to Many relationships.

Common use case for such pattern in JavaScript are Subjects by Rxjs which can be subscribed by the observers.

import { Subject } from 'rxjs';

const news = new Subject();

// add subscribers
const tv1 = news.subscribe(v => console.log(v + 'via TV station 1'));
const tv2 = news.subscribe(v => console.log(v + 'via TV station 2'));
const tv3 = news.subscribe(v => console.log(v + 'via TV station 3'));

// sending the event
news.next("Breaking news: ");

Mediator

A mediator is someone who handles all the inputs to it, process it, and provides different kinds of output accordingly.

It represents Many to Many relationships.

Examples of such mediators are the middlewares commonly used in express, which intercepts the incoming request.

const app = express();

function logger(req, res, next) {
    console.log('Request type:', req.method);
    next();
}

app.use(logger);

State

This pattern is applicable where objects behaves differently for a finite number of states.

This pattern again resembles O of S.O.L.I.D principles, i.e. Open/Closed Principle, which essentially warns us to use switch statements.

Instead of switch, classes can be used for each particular state.

Example -

class HappyState {
    think() {
        console.log("I am happy")
    }
}
class SadState {
    think() {
        console.log("I am sad")
    }
}
class NeutralState {
    think() {
        console.log("I am neutral")
    }
}

class User {
    constructor() {
        this.state = new HappyState();
    }

    think() {
        console.log(this.state.think())
    }

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

Here, without using a switch case, the state of the user can be changed via changeState method of the user.

Strategy

It’s same as state, but in this case, object behaves differently in the way it handles the data.

class EvenNumbers {
    perform(arr = []) {
        // ... print out array with only even numbers
    }
}

class OddNumbers {
    perform() {
        // ... print out array with only odd numbers
    }
}

class User {
    constructor() {
        this.strategy = new EvenNumbers();
    }

    perform(arr = []) {
        console.log(this.strategy.perform(arr))
    }

    changeStrategy(strategy) {
        this.strategy = strategy;
    }
}

Now, the user can change the strategy of how the data passed to it is handled, without using any switch statement.

Sources

10 Design Patterns Explained in 10 Minutes

10 Design Patterns Explained in 10 Minutes

Fireship

Sanjeet's LinkedIn

Composite Design Pattern | JavaScript Design Patterns

GeeksforGeeks

Sanjeet's LinkedIn

Decorator Design Pattern in JavaScript

GeeksforGeeks

Last updated on 15-08-2024