Design Patterns Implementation in JavaScript

Design Patterns Implementation in JavaScript

Common design patterns implemented in JS/TS

Design patterns are the battel-proof solution for common software design problems. Each pattern can be considered a generic template for a specific issue.

Design patterns are classified into these four categories,

  • Creational Design Pattern
  • Structural Design Pattern
  • Behavioral Design Pattern
  • Architectural Design Pattern

Here we will discuss a couple of common design patterns from JavaScript (or TypeScript) perspective,

  • Null Object
  • Factory [Creational]
  • Singleton [Creational]
  • Builder [Creational]
  • Prototype [Creational]
  • Decorator [Structure]
  • Facade [Structure]
  • Adapter [Structure]
  • Proxy [Structure]
  • Chain of Responsibility [Behavioral]
  • Command [Behavioral]
  • Observer [Behavioral]

Null Object Pattern


Consider the following examples, we have a band class. We create band objects using the name and their musical genres. Now we have a method that finds a band with name matching and then we print that particular band's genra.

It can happen in the getBand method, the name we pass as string, does not have a Band object. So it returns an undefined. In this case, when we try to read the genra from the band, it gives us an error of Cannot read properties of undefined (reading 'genre').

We have to add an additional check in the object, if the object exists only then the genra should be printed.

class Band {
    name: string;
    genra: string;

    constructor(name: string, genra: string) {
        this.name = name;
        this.genra = genra;
    }
}

const bands = [
    new Band('Warfaze', 'Heavy Metal'),
    new Band('Artcell', 'Progressive Rock Metal')
];

const getBand = (name: string) => {
    return bands.find(band => band.name === name);
}

// one way to print genre is, to check the object's existence beforehand
const myBand = getBand('Iron Maiden');
if (myBand !== undefined) {
  console.log(myBand.genra);
}

// otherwise it will throw the following error
// @ts-ignore
console.log(getBand('Iron Maiden').genra); // Cannot read properties of undefined (reading 'genra')

Using Null Object Pattern we can resolve the issue using a Null object, in this case, NullBand. When no matched null object is found, we use the NullBand object.

class Band {
    name: string;
    genra: string;

    constructor(name: string, genra: string) {
        this.name = name;
        this.genra = genra;
    }
}

class NullBand {
    name: string;
    genra: string;

    constructor() {
        this.name = '';
        this.genra = '';
    }
}

const bands = [
    new Band('Warfaze', 'Heavy Metal'),
    new Band('Artcell', 'Progressive Rock Metal')
];

const getBand = (name: string) => {
    return bands.find(band => band.name === name) || new NullBand;
}

console.log(getBand('Iron Maiden').genra);

Factory Pattern


Factory pattern allows creating an object using another object. It centralized the object creation logic.

For example, we have an organization where two types of employees,

  • Developer
  • Tester

We can create a developer and tester as follows,

function Developer(name) {
  this.name = name;
  this.jobType = 'Developer';
}

function Tester(name) {
  this.name = name;
  this.jobType = 'Tester';
}

const developer = new Developer('John');
const tester = new Tester('Doe');

console.log(developer); // Developer { name: 'John', jobType: 'Developer' }
console.log(tester); // Tester { name: 'Doe', jobType: 'Tester' }

Instead of using Factory Pattern we can create a factory object EmployeeFactory, that enables us to create new objects,

function Developer(name) {
  this.name = name;
  this.jobType = 'Developer';
}

function Tester(name) {
  this.name = name;
  this.jobType = 'Tester';
}

function EmployeeFactory (name, type) {
    switch(type) {
      case 'coding':
        return new Developer(name);
      case 'testing':
        return new Tester(name);
    }
}



const employee = [
  new EmployeeFactory('John', 'coding'),
  new EmployeeFactory('Doe', 'testing')
];

/**
 * Output
 * Name: John Role: Developer
 * Name: Doe Role: Tester
 * /
employee
  .map(({ name, jobType }) => console.log(`Name: ${name} Role: ${jobType}`));

Singleton Pattern


There could be some cases, where we do not want to create instances of a class more than once. Rather, we use one single instance of the class globally.

Consider we have a log manager and we collect logs from all over the application. We do want to store all these logs in a single object rather than using multiple objects. Without a singleton, each object holds its own logs,

class LogManager {
  constructor() {
    this.logs = [];
  }

  insert(log) {
    this.logs.push(log);
  }

  getLogs() {
    return this.logs;
  }
}

// Log from file 1
const logManager1 = new LogManager();
logManager1.insert('My Log 1');

// log from file 2
const logManager2 = new LogManager();
logManager2.insert('My Log 2');

// Want to see all the logs
console.log(logManager1.getLogs()); // ['My Log 1']
console.log(logManager2.getLogs()); // ['My Log 2']

With the singleton pattern, we ensure, the log manager does not create multiple instances. So with a single object, we can get all the logs in one place.

let instance;
class LogManager {
  constructor() {
    if (instance) {
      return instance;
    }

    this.logs = [];
    instance = this;
  }

  insert(log) {
    this.logs.push(log);
  }

  getLogs() {
    return this.logs;
  }
}

// Log from file 1
const logManager1 = new LogManager();
logManager1.insert('My Log 1');

// log from file 2
const logManager2 = new LogManager();
logManager2.insert('My Log 2');

// Want to see all the logs
console.log(logManager1.getLogs()); // ["My Log 1", "My Log 2"]
console.log(logManager2.getLogs()); // ["My Log 1", "My Log 2"]

Builder Pattern


Consider the following example, where we create band information with name, lineup, genre and origin. When we only have name and origin but the lineup and genra is missing, during the creation of an object, we have to pass two undefined value.

class Band {
  constructor(name, lineup, genra, origin) {
    this.name = name;
    this.lineup = lineup;
    this.genra = genra;
    this.origin = origin;
  }
}

const warfaze = new Band('Warfaze', undefined, undefined, 'Dhaka');

console.log(warfaze);

With JS way, we can resolve it using JS specific builder pattern, where we only take the name but other properties will be passed as object properties. In this case, we do not have to do with undefined value.

class Band {
  constructor(name, {
    lineup,
    genra, 
    origin
  } = {}) {
    this.name = name;
    this.lineup = lineup;
    this.genra = genra;
    this.origin = origin;
  }
}

const warfaze = new Band('Warfaze', {
  origin: 'Dhaka'
});

console.log(warfaze);

We can solve this in a traditional way like the following also.

class Band {
  constructor(name, lineup, genra, origin) {
    this.name = name;
    this.lineup = lineup;
    this.genra = genra;
    this.origin = origin;
  }
}

class BandBuilder {
  constructor(name) {
    this.band = new Band(name)
  }

  setLineup(lineup) {
    this.band.lineup = lineup;
    return this;
  }

  setGenra(genra) {
    this.band.genra = genra;
    return this;
  }

  setOrigin(origin) {
    this.band.origin = origin;
    return this;
  }

  build() {
    return this.band;
  }
}

const warfaze = new BandBuilder('Warfaze').setOrigin('Dhaka').build();

console.log(warfaze);

Prototype Pattern


With es6, the prototype pattern in JS is just like classical class inheritance. It allows extending functionality to the inherited classes without being copied them.

In the following example, we have a Shape class that extends by Circle and Rectangle.

Then the Rectangle class inherits the Square class.

The base class Shape has a method logInfo and logInfo method is available to all the inherited classes. We can use logInfo class from Circle, Reactange and Square class objects.

This logInfo class does not copy to its inherited classes, instead available in just the Shape class memory.

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

  logInfo() {
    console.log(this);
  }
}

class Circle extends Shape {
  constructor(name) {
    super(name);
  }
}

class Reactangle extends Shape {
  constructor(name, width, height) {
    super(name);
    this.width = width;
    this.height = height;
  }
}

class Square extends Reactangle {
  constructor(name, width) {
    super(name, width, width);
  }
}

const circle = new Circle('circle');
circle.logInfo(); // Circle { name: 'circle' }
const reactangle = new Reactangle('reactangle', 10, 20);
reactangle.logInfo(); // Reactangle { name: 'reactangle', width: 10, height: 20 }
const square = new Square('square', 15);
square.logInfo(); // Square { name: 'square', width: 15, height: 15 }

Decorator Pattern


Decorator Pattern allows updating the behavior of the existing classes.

In the following example, we have a Vehicle method, that is used to create objects. Now after creating a vehicle object, we can see the name, model, and price from the object.

We will use two types of decorators,

  • One to update the model
  • Another to update the vehicle price if AC Sub Engine is added to the vehicle
function Vehicle(name) {
  this.name = name;
  this.model = 'Default';
  this.getPrice = () => {
    return 2;
  };
}

const bus = new Vehicle('Volvo');

// Decorator 1
bus.setModel = function (model) {
  this.model = model;
}
bus.setModel('B7R');

// Decorator 1
function IncludeAcSubEngine(bus) {
  const existingPrice = bus.getPrice();
  bus.getPrice = function() {
    return existingPrice + 1;
  }
}

console.log(bus.name, bus.model, bus.getPrice()); // Volvo B7R 2

IncludeAcSubEngine(bus);
console.log(bus.name, bus.model, bus.getPrice()); // Volvo B7R 3

Facade Design Patterns


In our system, after checkout, consider using stripe for payment.

// initiate stripe
const stripe = {
  pay: (amount) => {}
};

const purchase = (itemName, price) => {
  stripe.pay(price);
}

Later it can happen, stripe becomes expensive compared to PayPal, and want to update the payment system from stripe to PayPal. To do this, we need to update the purchase method.

// initiate PayPal
const paypal = {
  pay: (amount) => {}
};

const purchase = (itemName, price) => {
  paypal.pay(price);
}

Instead, if we isolate the functionalities of payment in a separate module, this code refactoring will be much easier,

const purchase = (itemName, price) => {
  makePayment(price);
}

const makePaymet = (price) => {
  // initialize stripe or paypal
  // implement the payment functionality with stripe/paypal 
}

In this case, there is a wrapper over the stripe/PayPal module and it will help keep the existing codes without modification.

Another example can be, data fetching functionality. Lets say we use axios for fetching data. Later we may decide to use the native fetch module. If all the data fetching functionalities are in a separate module, it would be easy to refactor the code base later during switching between fetch and axios module.

Adapter Pattern


When two functionality is not compatible, an adapter pattern comes between them and make them compatible. A real-world scenario can be, we used 3.5 mm audio jack and later typeC input has come. To use the typeC input with a 3.5 mm audio jack, we do use a converter or adapter.

Consider following calculator class, it simply do addition and substraction,

class Calculator {
  constructor(num1, num2) {
    this.num1 = num1;
    this.num2 = num2;
  }

  operation(operationType) {
    switch(operationType) {
      case 'ADD':
        return this.num1 + this.num2;
      case 'SUBSTRACT':
        return this.num1 - this.num2;
      default:
        return NaN;
    }
  }
}

const calculator = new Calculator(10, 6);
console.log(calculator.operation('ADD')); // 16
console.log(calculator.operation('SUBSTRACT')); // 4

Now later, we come up new calculator core functionality and it has better performance,

class CalculatorCore {
  constructor(num1, num2) {
    this.num1 = num1;
    this.num2 = num2;
  }

  add() {
    return this.num1 + this.num2;
  }

  substract() {
    return this.num1 - this.num2;
  }
}

This new CalculatorCore is not compatible with our existing class, it does not have the operation method and also does not take the operationType. In this case, we can make use of the Adapter Pattern to make our existing class compatible with the new Calculator Core class,

class CalculatorCore {
  constructor(num1, num2) {
    this.num1 = num1;
    this.num2 = num2;
  }

  add() {
    return this.num1 + this.num2;
  }

  substract() {
    return this.num1 - this.num2;
  }
}

class CalculatorAdapter {
  constructor(num1, num2) {
    this.calculator = new CalculatorCore(num1, num2);
  }

  operation(operationType) {
    switch(operationType) {
      case 'ADD':
        return this.calculator.add();
      case 'SUBSTRACT':
        return this.calculator.substract();
      default:
        return NaN;
    }
  }
}

const calculatorAdapter = new CalculatorAdapter(10, 6);
console.log(calculatorAdapter.operation('ADD')); // 16
console.log(calculatorAdapter.operation('SUBSTRACT')); // 4

Proxy Design Pattern


Proxy design pattern creates the same class similar to the original implementation and reduces operation. Consider following CurrencyAPI class. The method getCurrency take a coinName and fetch a long running api call to get the currency.

If we look for the currency of a bitCoin it will fetch the API every single time. We can reduce the operation by making use of a Proxy Class.

class CurrencyAPI {
  // long running api call
  getCurrency(coinName) {
    switch(coinName) {
      case 'BitCoin':
        return '100$';
      case 'Ethereium':
        return '80$';
      default: 
        return 'Invalid Coin';
    }
  }
}

const fetch = new CurrencyAPI();
// getting from long api call
console.log(fetch.getCurrency('BitCoin')); // 100$
// getting from long api call
console.log(fetch.getCurrency('BitCoin')); // 100$

According to the Proxy Design Pattern, we create a similar class called ProxyCurrencyAPI. It uses the original CurrencyAPI and additionally caches the result. For the first time, it fetches the API, caches it, and from the next call, it retrieves the results from the cache.

class CurrencyAPI {
  getCurrency(coinName) {
    switch(coinName) {
      case 'BitCoin':
        return '100$';
      case 'Ethereium':
        return '80$';
      default: 
        return 'Invalid Coin';
    }
  }
}

class ProxyCurrencyAPI {
  cache = {};

  constructor() {
    this.fetch = new CurrencyAPI();  
  }

  getCurrency(coinName) {
    if (this.cache[coinName]) {
      return this.cache[coinName];
    }
    this.cache[coinName] = this.fetch.getCurrency(coinName);
    return this.cache[coinName];
  }
}

const fetch = new ProxyCurrencyAPI();

// fetch from api
console.log(fetch.getCurrency('BitCoin')); // 100$
// retrieve from cache
console.log(fetch.getCurrency('BitCoin')); // 100$

Chain of Responsibility Pattern


With the Chain of Responsibility Pattern, if one object can not resolve an operation, it passes the operation to its successor.

In the following example, we have Managerial handled that tends to handle Manager types operation. In case it does not resolve the operation, it passes the operations to its successor,

class Handler {
  constructor() {
    this.successor = null;
  }

  setSuccessor(successor) {
    this.successor = successor;
  }
}

class AdminHandler extends Handler {
  handleOperation(operationType) {
    if (operationType === 'Admin') {
      console.log('Handled By Admin');
      return;
    }
  }
}

class ManagerialHandler extends Handler {
  handleOperation(operationType) {
    if (operationType === 'Manager') {
      console.log('Handled By Manager');
      return;
    }

    if (this.successor) {
      this.successor.handleOperation(operationType);
    }
  }
}

const magerialHandler = new ManagerialHandler();
magerialHandler.handleOperation('Manager'); // Handled By Manager

const adminHandler = new AdminHandler();
magerialHandler.setSuccessor(adminHandler);
magerialHandler.handleOperation('Admin'); // Handled By Admin

Command Pattern


Command pattern allows undoing an operation. This can be very useful in some scenarios, like inserting a user in DB and later deciding to undo the operation.

In the following example, the calculator takes a command (EX. AddCommand). It can perform operations and later be able to undo the operation.

class Calculator {
  constructor() {
    this.value = 0;
    this.history = [];
  }

  executeCommand(command) {
    this.value = command.execute(this.value);
    this.history.push(command);
  }

  undo() {
    const command = this.history.pop();
    this.value = command.undo(this.value);
  }
}

class AddCommand {
  constructor(valueToBeAdded) {
    this.valueToBeAdded = valueToBeAdded;
  }

  execute(currentValue) {
    return currentValue + this.valueToBeAdded;
  }

  undo(currentValue) {
    return currentValue - this.valueToBeAdded;
  }
}

const calculator = new Calculator();
calculator.executeCommand(new AddCommand(5));
console.log(calculator.value); // 5
calculator.executeCommand(new AddCommand(7));
console.log(calculator.value); // 12
calculator.undo(); // 5
console.log(calculator.value);

Observer Pattern


In the observer pattern, there is a subject and a couple of observers. Observers are methods, acting as subscribers to the subjects. These observers subscribe to the subject and run according to the changes in the subject.

In the following example, we have a subject, where we can add or remove subscribers.

We can run all the observers using runObservers method after subscribing them. Later it is possible to run this observer in a certain value changes in the Subject.

class Subject {
  constructor() {
    this.observer = [];
  }

  subscribe(fn) {
    this.observer.push(fn);
  }

  unsubscribe(fn) {
    this.observer = this.observer.filter(f => f !== fn);
  }

  runObservers() {
    this.observer.map(fn => fn());
  }
}


const subject = new Subject();

const observer1 = () => console.log('I am observer1 method.');
const observer2 = () => console.log('I am observer2 method.');

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.runObservers();

Resources