JavaScript Design Patterns

JavaScript Design Patterns in ES 2015

View project on GitHub
NOTE: code samples are inside gh-pages branch and you can see the output inside the console (CTRL+OPT+I)

Constructor Pattern

The below example is a simple way to setup a constructor which is used to instantiate a new object. Before ES6 this would look a little different, you don't have the Class keyword available and you would just create a constructor function.


/*
 * Constructor Pattern
 */
class Car {
  constructor(opts) {
    this.model = opts.model;
    this.year = opts.year;
    this.miles = opts.miles;
  }

  // Prototype method
  toString() {
    return `${this.model} has driven ${this.miles} miles`;
  }
}

// Usage:
var civic = new Car({
  model: 'Honda',
  year: 2001,
  miles: 50000
});

// ES5 Example of constructor function
function Car() {
  this.model = opts.model;
  ...
  ...
}
Car.prototype.toString = function() {
  ...
}

Module Pattern

Classes in ES6 are also the new way of doing the Module Pattern which simply allows you to keep units of code cleanly separated and organized. The main difference with ES6 classes are how you handle private encapsulation. The example below shows a basic example of private encapsulation.


/* lib/module.js */
const shoppingList = new WeakMap();

/**
 * Module Pattern
 */
class AbstractDataType {
  constructor() {
    // woo! our Class is instantiated lets add some private properties.
    shoppingList.set(this, ['coffee', 'chicken', 'pizza'])
  }

  // Lets create a public prototype method to access our private `shoppingList`
  getShoppingList() {
    return shoppingList.get(this);
  }

  addItem(item) {
    shoppingList.get(this).push(item);
  }
}

// Now we export our `Class` which will become an importable module.
export default AbstractDataType;

WeakMap is also a new ES6 feature that seems to be a good option for creating private memebers. It accepts any value as a key and doesn't prevent the key object from getting garbage collected when the WeakMap object still exists. It remove any entry containing the object once it's garbage collected.

If you haven't been following Modules for ES6 the export default AbstractDataType is simply exporting our class so we can later use import AbstractDataType from 'libs/module';. There is a great article going further in depth with how the module system works here.

What benefit does private encapsulation offer JavaScript? Well one of the huge benefits is having a flexible API, in the long run you can keep public methods frozen but still change internal private methods to make the internals more robust. That way as additional complexity is introduced to fulfill features requirements you don't have to worry about breaking other components. Another benefit is you can restrict access to things you don't want malicious users to use or see. ( not full proof since everything on the client-side is always available to the client, it's better to rely on server-side for security. )

Observer Pattern

Another popular pattern that is being simplified in ES6/ES7 is the Observer Pattern. If you're not familiar the Observer Pattern is a design pattern in which an object (known as the subject) maintains a list of objects depending on what it observes (observers), automatically notifying them of any changes to state. In ES5 this was a little cumbersome to setup but not difficult. You created a ObserverList constructor function which creates an empty list of observers. After that you create an interface of public prototype methods for tasks like adding, deleting, etc. Now it's much more simple we have a handy dandy Object.observe method we can use to do the same job with a lot less code.


/*
 * Observer Pattern
 */
var model = {};

Object.observe(model, function(changes) {
  // async callback
  changes.forEach(function(change) {
    console.log(change.type, change.name, change.oldValue);
  });
});

Publish/Subscribe Pattern

Another simple but useful pattern is the Publish/Subscribe Pattern. Publish/Subscribe is just another variant of the Observer Pattern. The main difference is that the Observer Pattern requires that the observer receiving notifications must subscribe to the object firing the event (the subject). The Publish/Subscribe Pattern however uses a topic/event channel that sits between the objects waiting to receive notifications (subscribers) and waiting for the object firing the events (the publisher). This allows you to avoid dependencies between the subscriber and publisher.


/*
 * Publish/Subscribe Pattern
 */
class PubSub {
  constructor() {
    this.handlers = [];
  }

  subscribe(event, handler, context) {
    if (typeof context === 'undefined') { context = handler; }
    this.handlers.push({ event: event, handler: handler.bind(context) });
  }

  publish(event, args) {
    this.handlers.forEach(topic => {
      if (topic.event === event) {
        topic.handler(args)
      }
    })
  }
}

/*
 * Simple ChatRoom Class
 * uses the PubSub Class to notify other users when a message is sent.
 */
class ChatRoom {
  constructor() {
    this.pubsub = new PubSub();
    this.pubsub.subscribe('message', this.emitMessage, this);
  }

  emitMessage(msg) {
    console.group('PubSub')
    console.log('user sent message!', msg);
    console.groupEnd();
  }

  sendMessage() {
    this.pubsub.publish('message', 'Hey, how are you?');
  }

}

var room = new ChatRoom();
room.sendMessage();

Mediator Pattern

If you're not familiar with the Mediator Pattern it's a pattern that acts as a unified interface through which the different components of an application may communicate. If a unit of code has too many direct relationships between components, it may be time to have a central point of control that components can communicate through.

A traffic light (mediator) handles which cars can go and stop, because all communications (notifications) are performed from the traffic light, rather than from car to car. Could you imagine communicating to other drivers as they come by? :)

/*
 * Publish/Subscribe Pattern
 */
class PubSub {
  constructor() {
    this.handlers = [];
  }

  subscribe(event, handler, context) {
    if (typeof context === 'undefined') { context = handler; }
    this.handlers.push({ event: event, handler: handler.bind(context) });
  }

  publish(event, args) {
    this.handlers.forEach(topic => {
      if (topic.event === event) {
        topic.handler(args)
      }
    })
  }
}

/*
 * Mediator Pattern
 */
class Mediator extends PubSub {
  constructor(opts) {
    super();
  }

  attachToObject(obj) {
    obj.handlers = [];
    obj.publish = this.publish;
    obj.subscribe = this.subscribe;
  }
}

var mediator = new Mediator();

var myRoom = {
  name: 'myRoom'
};

mediator.attachToObject(myRoom);

myRoom.subscribe('connection', function() {
  console.group('Mediator');
  console.log(`user connected to ${myRoom.name}!`);
  console.groupEnd();
}, myRoom);

myRoom.publish('connection');