Creational Design Patterns in JavaScript

What is a Pattern?

A pattern is a reusable solution that can be applied to commonly occurring problems in software engineering. One of the first and important work published on design patterns in software engineering was a book in 1995 called Design Patterns: Elements Of Reusable Object-Oriented Software written by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides – a group that became known as the Gang of Four (GoF).

Design patterns benefits

  • They are proven/documented solutions
  • They are easily reusable
  • They are expressive – can easily explain a large solution
  • They prevent the need for refactoring code
  • They lower the size of the codebase by avoiding repetition
  • Ease developers communication

The Structure Of A Design Pattern

  • Pattern name and description
  • Context outline – the contexts in which the pattern is effective
  • Problem statement – what problem it solves
  • Solution – a description of how the user’s problem is being solved
  • Design – description of the pattern’s design, mainly the user’s behavior
  • Implementation – a guide of pattern implementation
  • Illustrations – a visual representation of classes in the pattern (diagrams)
  • Examples – a minimal implementation of the pattern
  • Co-requisites – other patterns needed to support the used the pattern
  • Relations – does this pattern resemble other ones ?
  • Known usage (popularity) – where and how is used already this pattern
  • Benefits

Anti-Patterns

If a pattern represents a best practice, an anti-pattern describe a bad solution and how to get out of it.

Categories of Design Patterns

Design patterns are divided in 3 main categories: Creational, Structural and Behavioral.

Constructor Pattern

In object-oriented programming languages, a constructor is a special method used to initialize a newly created object once memory has been allocated for it. JavaScript has 3 ways to create new objects:
let newObj = {};
// or
let newObj = Object.create(Object.prototype);
// or
let newObj = new Object();
There are 4 ways in which keys and values can be assigned to an object:
// 1. dot syntax
newObj.key = "hello";
console.log(newObj.key);

// 2. square brackets notation
newObj["key"] = "hello";
console.log(newObj["key"]);

// 3. Object.defineProperty
Object.defineProperty(newObj, "key", {
    value: "hello",
    writable: true,
    enumerable: true,
    configurable: true
});

// 4. Object.defineProperties
Object.defineProperties(newObj, {
  "someKey": {
    value: "hey",
    writable: true
  },
  "anotherKey": {
    value: "hola",
    writable: false
  }
});
There are 2 ways to define a class:
function Phone(name, brand, price) {
    this.name = name;
    this.brand = brand;
    this.price = price;
    this.displaySpecs = function () {
        console.log(`Phone ${this.name} from ${this.brand} costs ${this.price}`);
    }
}
// or
class Phone {
    constructor(name, brand, price) {
        this.name = name;
        this.brand = brand;
        this.price = price;
    }
    displaySpecs = function () {
        console.log(`Phone ${this.name} from ${this.brand} costs ${this.price}`);
    }
}

let samsung = new Phone('galaxy s6', 'samsung', 700);
let iphone = new Phone('iphone 12 pro', 'apple', 1200);

console.log(samsung.displaySpecs === iphone.displaySpecs); // false
The above constructor pattern makes inheritance difficult and redefines the displaySpecs method for each instance of Phone.

Prototype Pattern

Every JavaScript object has a prototype which is an object. All JavaScript objects inherit their properties and methods from their prototype. Let’s rewrite the Phone class using the prototype so all its instances will share the same displaySpecs method.
function Phone(name, brand, price) {
    this.name = name;
    this.brand = brand;
    this.price = price;
}

Phone.prototype.displaySpecs = function () {
    console.log(`Phone ${this.name} from ${this.brand} costs ${this.price}`);
}

let samsung = new Phone('galaxy s6', 'samsung', 700);
let iphone = new Phone('iphone 12 pro', 'apple', 1200);

console.log(samsung.displaySpecs === iphone.displaySpecs); // true

Module Pattern

JavaScript modules are self-contained code blocks, that perform specific work. The module pattern allows encapsulation: the variables and functions are kept private inside the module body and can’t be overwritten. The old way of creating a module is by using Immediately-Invoked-Function-Expressions (IIFE):
(function() {
    // declare private variables and/or functions

    return {
        // declare public variables and/or functions
    }
})();
Example:
let BillModule = (function() {
    let bills = [];

    function addBill(name) {
        bills.push(name);
    }

    function getBills() {
        return bills;
    }

    function removeBill(name) {
        const index = bills.indexOf(name);
        if(index < 1) {
            throw new Error('Bill not found...');
        }
        bills.splice(index, 1)
    }

    return {
    	manageBills: function(name) {
    		if (name) addBill(name);
    		console.log(getBills());
    	}
    };
})();

BillModule.manageBills('Water'); // ["Water"]
console.log(BillModule.bills);  // undefined
A variation of the module pattern is the Revealing Module Pattern which expose certain variables and methods returned in an object literal:
function BillModule() {
    const bills = [];

    function addBill(name) {
        bills.push(name);
    }

    function getBills() {
        return bills;
    }

    function removeBill(name) {
        const index = bills.indexOf(name);
        if(index < 1) {
            throw new Error('Bill not found...');
        }
        bills.splice(index, 1)
    }

    return {
        add: addBill,
        get: getBills,
        remove: removeBill
    }
}

const bills = BillModule();
bills.add('Water');
bills.add('Gas');
bills.add('Electricity');

console.log(bills.get()) //Array(3) ["Water", "Gas", "Electricity"]
bills.remove('Gas')
console.log(bills.get()); //Array(2) ["Water", "Electricity"]
Let's see a better version using ES6 modules:
// bill.js
class BillModule {
	bills = [];
	constructor(data) {
		this.bills = data;
	}
	addBill(name) {
        this.bills.push(name);
    }
    getBills() {
        return this.bills;
    }
    removeBill(name) {
        const index = this.bills.indexOf(name);
        if(index < 1) {
            throw new Error('Bill not found...');
        }
        bills.splice(index, 1)
    }
}

export default BillModule;

// dashboard.js
import BillModule from 'bill';
const bill = new BillModule(['Water', 'Gas']);
console.log(bill.getBills());

Factory pattern

Imagine creating a Notification Management app which currently allows only notifications through Email. If you need to add new types of notifications (objects) with similar features you might end up in a lot of repetition. To solve this complexity, we will delegate (abstract) the object creation to another object called Factory.
class PushNotification {
 constructor(sendTo, message) {
   this.sendTo = sendTo;
   this.message = message;
 }
}

class PopupNotification {
  constructor(title, message) {
   this.title = title;
   this.message = message;
 }
}
 
class EmailNotification {
 constructor(sendTo, cc, emailContent) {
   this.sendTo = sendTo;
   this.cc = cc;
   this.emailContent = emailContent;
 }
}

class NotificationFactory {
 createNotification(type, props) {
   switch (type) {
     case 'email':
       return new EmailNotification(props.sendTo, props.cc, props.emailContent);
     case 'push':
       return new PushNotification(props.sendTo, props.message);
     case 'popup':
       return new PopupNotification(props.title, props.message);
   }
 }
}
 
const factory = new NotificationFactory();
 
const emailNotification = factory.createNotification('email', {
 sendTo: 'receiver@domain.com',
 cc: 'other@domain.com',
 emailContent: 'email content',
});
 
const pushNotification = factory.createNotification('push', {
 sendTo: 'receiver-id',
 message: 'message',
});

const popupNotification = factory.createNotification('popup', {
 title: 'hi',
 message: 'message',
});

Singleton pattern

Singleton pattern ensures the instance of an object is created only once. An example is a database connection pool that manages the creation, destruction and lifetime of all database connections for the entire application ensuring that no connection is 'lost'.
function DatabaseConnection() {
    let dbInstance = null; 
    let count = 0;

    // Lazy Initialization
    function init() {
        console.log(`open db #${count + 1}`);
    }

    function createIntance() {
        if (!dbInstance) {
            dbInstance = init();
        }
        return dbInstance;
    }

    function closeIntance() {
        console.log('close db');
        dbInstance = null;
    }

    return {
        open: createIntance,
        close: closeIntance
    }
}

const dbConn = DatabaseConnection();
dbConn.open(); // open db #1
dbConn.open(); // open db #1
dbConn.open(); // open db #1
dbConn.close(); // close db
ES6 Modules are singletons, thus all you have to do is define your class/object in a module.
// dbcon.js
export class DB {
  count = 0;
  constructor() {
    this.count++;
    console.log(`open db #${this.count}`);
  }
}

// app.js
import DB from './dbcon';
console.log(new DB().count); // 1

// index.js
import DB from './dbcon';
console.log(new DB().count); // 1

Builder pattern

Builder is a creational design pattern which allows constructing complex objects step by step. Construction details are hidden from the client entirely. The objects participating in this pattern:
  • Director: constructs products by using the Builder's multistep interface
  • Builder: declares a multistep interface for creating a complex product
  • ConcreteBuilder:
    • implements the Builder interface
    • retrieve the newly created product
  • Products: the complex objects

// typescript would be better as it supports interfaces
// emulate abstract class
class Notification {
  constructor(body, subject) {
    this.id = new Date().getTime();
    this.body = body;
    this.subject = subject;
    this.sent = false;
    if (new.target === Notification) {
      throw new TypeError("Cannot construct Notification instances directly");
    }
  }

  status() {
    console.log("sent: ", this.sent);
    console.log(
      this.id + "\n" + this.to + "\n" + this.body + "\n" + this.subject
    );
  }
}

// abstract class and methods
class Builder {
  constructor() {
    if (new.target === Builder) {
      throw new TypeError("Cannot construct Builder instances directly");
    }
    if (!this.send || !this.setTemplate) {
      // or (typeof this.send === "undefined")
      // or (typeof this.send === "function")
      throw new TypeError("Must implement interface methods");
    }
  }
}

class SMSNotification extends Notification {
  constructor(body, subject, to) {
    super(body, subject);
    this.to = to;
  }
}

class EmailNotification extends Notification {
  constructor(body, subject, to, cc, bcc) {
    super(body, subject);
    this.to = to;
    this.cc = cc;
    this.bcc = bcc;
  }
  resendEmail() {}
  status() {
    super.status();
    console.log(this.cc, this.bcc);
  }
}

class SMSBuilder extends Builder {
  sms = null;

  init(body, subject, to) {
    this.sms = new SMSNotification(body, subject, to);
  }
  setupGateway() {}
  setTemplate(id) {}
  send() {
    this.sms.sent = true;
  }
  getNotification() {
    return this.sms;
  }
}

class EmailBuilder extends Builder {
  email = null;

  init(body, subject, to, cc, bcc) {
    this.email = new EmailNotification(body, subject, to, cc, bcc);
  }
  setupEmailServer() {}
  setTemplate(id) {}
  send() {
    this.email.sent = true;
  }
  getNotification() {
    return this.email;
  }
}

class Admin {
  createSMSNotification(builder) {
    builder.init("sms long text", "hello", "0349990022");
    builder.setupGateway();
    builder.setTemplate(4);
    builder.send();
    return builder.getNotification();
  }

  createEmailNotification(builder) {
    builder.init(
      "email long text",
      "email title",
      "ana@yahoo.com",
      "bob@gmail.com"
    );
    builder.setupEmailServer();
    builder.setTemplate(1);
    builder.send();
    return builder.getNotification();
  }
}

let admin = new Admin();

let smsBuilder = new SMSBuilder();
let sms = admin.createSMSNotification(smsBuilder);
sms.status();

let emailBuilder = new EmailBuilder();
let email = admin.createEmailNotification(emailBuilder);
email.status();
In some cases the pattern can be simplified by using only the Product and ConcreteBuilder objects:
class EmailNotification {
  constructor(builder) {
    this.id = builder.id;
    this.body = builder.body;
    this.subject = builder.subject;
    this.to = builder.to;
    this.cc = builder.cc;
    this.bcc = builder.bcc;
    this.sent = builder.sent;
  }
}

class EmailBuilder {
  body = "";
  subject = "";
  id = 0;

  constructor(body, subject) {
    this.body = body;
    this.subject = subject;
  }

  init() {
    this.id = new Date().getTime();
    return new EmailNotification(this);
  }
  setupEmailServer(to, cc, bcc) {
    this.to = to;
    this.cc = cc;
    this.bcc = bcc || "";
    return this;
  }
  setTemplate(id) {
    return this;
  }
  send() {
    this.sent = true;
    return this;
  }
}

let email = new EmailBuilder("email long text", "email title")
  .setupEmailServer("ana@yahoo.com", "bob@gmail.com")
  .setTemplate(1)
  .send()
  .init();
email-newsletter

Dev'Letter

Professional opinion about Web Development Techniques, Tools & Productivity.
Only once a month!

Any suggestion on what to write next time ? Submit now

Opinions

avatar
550
  Subscribe  
Notify of

Related Posts