Design Patterns in JavaScript

Design Patterns in JavaScript

Design patterns are advanced object-oriented solutions to commonly occurring software problems. Patterns are about reusable designs and interactions of objects. Each pattern has a name and becomes part of a vocabulary when discussing complex design solutions.

designPatterns.JPG

The main benefits we get from design patterns are the following:

  • They are easily reusable: Design patterns document a reusable solution which can be modified to solve multiple particular problems, as they are not tied to a specific problem.
  • They are expressive: Design patterns can explain a large solution quite elegantly.
  • They ease communication: When developers are familiar with design patterns, they can more easily communicate with one another about potential solutions to a given problem.
  • They lower the size of the codebase: Because design patterns are usually elegant and optimal solutions, they usually require less code than other solutions.

Every developer strives to write maintainable, readable, and reusable code. Code structuring becomes more important as applications become larger. JavaScript web developers frequently interact with design patterns, even unknowingly, when creating applications.

des1.JPG

Constructor Design Pattern

This is a special method that is used to initialize the newly created objects once a memory is allocated. Since JavaScript is typically object-oriented, it deals with objects most, therefore I intend to delve in to object constructors. There are three ways to create new objects in JavaScript:

The following is one way to create a constructor design pattern.

// This creates a new empty Object
var newObject = {};

// This creates a new empty Object
var newObject = Object.create(Object.prototype);

var newObject = newObject();

To access the properties of a function, you need to initialize the object.

const object = new ConstructorObject();

Whereby the new keyword above tells JavaScript that a constructorObject should act as a constructor.

The Singelton Pattern

Singleton pattern can be implemented by creating a class with a method that creates a new instance of the class if one doesn’t exist. If instance already existing, it simply returns a reference to that object.

var mySingleton = (function () {
  // Instance stores a reference to the Singleton
  var instance;
  function init() { // Singleton };
  return {
    // Get the Singleton instance if one exists
    // or create one if it doesn't
    getInstance: function () {
      if ( !instance ) {  // check for the instance 
        instance = init(); 
      }
      return instance;  
    }
  };
})();

The Prototype Pattern

It is based on prototypal inheritance where we create objects which act as prototypes for other objects. It requires the use of Object.create.

It is worth noting that prototypal relationships can cause trouble when enumerating properties of objects and wrapping the contents of the loop in a hasOwnProperty() check.

It is worth noting that prototypal relationships can cause trouble when enumerating properties of objects and wrapping the contents of the loop in a hasOwnProperty() Object.create also allows us to easily implement advanced concepts such as differential inheritance where objects are able to directly inherit from other objects.

var myPhone = {
  name: "Samsung",
  drive: function () {
    console.log( "This is really cool !" );
  },
  panic: function () {
    console.log( "How to make the camera capture Mars ? " );
  }
};

// Use Object.create to instantiate a new phone
var yourPhone = Object.create( myPhone);

// Now we can see that one is a prototype of the other
console.log( yourPhone.name );

Adapter Pattern

This is a structural pattern where the interface of one class is translated into another.

This pattern lets classes work together that could not otherwise because of incompatible interfaces.

This pattern is often used to create wrappers for new refactored APIs so that other existing old APIs can still work with them.

This is usually done when new implementations or code refactoring (done for reasons like performance gains) result in a different public API, while the other parts of the system are still using the old API and need to be adapted to work together.

// old interface
class OldCalculator {
  constructor() {
    this.operations = function(term1, term2, operation) {
      switch (operation) {
        case 'add':
          return term1 + term2;
        case 'sub':
          return term1 - term2;
        default:
          return NaN;
      }
    };
  }
}

// new interface
class NewCalculator {
  constructor() {
    this.add = function(term1, term2) {
      return term1 + term2;
    };
    this.sub = function(term1, term2) {
      return term1 - term2;
    };
  }
}

// Adapter Class
class CalcAdapter {
  constructor() {
    const newCalc = new NewCalculator();

    this.operations = function(term1, term2, operation) {
      switch (operation) {
        case 'add':
          // using the new implementation under the hood
          return newCalc.add(term1, term2);
        case 'sub':
          return newCalc.sub(term1, term2);
        default:
          return NaN;
      }
    };
  }
}

// usage
const oldCalc = new OldCalculator();
console.log(oldCalc.operations(20, 15, 'add')); // 35

const newCalc = new NewCalculator();
console.log(newCalc.add(20, 15)); // 35

const adaptedCalc = new CalcAdapter();
console.log(adaptedCalc.operations(20, 15, 'add')); // 35;

Observer Pattern

The observer design pattern is handy in a place where objects communicate with other sets of objects simultaneously. In this observer pattern, there is no unnecessary push and pull of events across the states, rather the modules involved only modify the current state of data.

function Observer() {
this.observerContainer = [];
}

Observer.prototype.subscribe = function (element) {
this.observerContainer.push(element);
}

// the following removes an element from the container

Observer.prototype.unsubscribe = function (element) {

const elementIndex = this.observerContainer.indexOf(element);
if (elementIndex > -1) {
this.observerContainer.splice(elementIndex, 1);
}
}

/**
* we notify elements added to the container by calling
* each subscribed components added to our container
*/
Observer.prototype.notifyAll = function (element) {
this.observerContainer.forEach(function (observerElement) {
observerElement(element);
});
}

The Facade Pattern

Convenient, high-level interfaces to larger bodies of code that hide underlying complexity

When you put up a facade, you’re usually creating an outward appearance which conceals a different reality. Think of it as simplifying the API presented to other developers

var module = (function () {  
    var _private = {  
        i: 5,
        get: function () {  
            console.log('current value:' + this.i);  
        },
        set: function (val) {  
            this.i = val;  
        },
        run: function () {  
            console.log('running');  
        },
        jump: function () {  
            console.log('jumping');  
        }  
    };
    return {  
        facade: function (args) {
        // set values of private properties 
        _private.set(args.val);
        // test setter
        _private.get();
        // optional: provide a simple interface
        // to internal methods through the
        // facade signature
            if (args.run) {  
                _private.run();  
            }  
        }  
    }
}());

Decorator Pattern

Decorators are a structural JS design pattern that aims to promote code reuse. This pattern allows behavior to be added to an individual object dynamically, without affecting the behavior of other objects from the same class. Decorators can also provide a flexible alternative to subclassing for extending functionality.

// A vehicle constructor
function Vehicle( vehicleType ){

    // some sane defaults
    this.vehicleType = vehicleType || "car";
    this.model = "default";
    this.license = "00000-000";

}

// Test instance for a basic vehicle
var testInstance = new Vehicle( "car" );
console.log( testInstance );

// Outputs:
// vehicle: car, model:default, license: 00000-000

// Lets create a new instance of vehicle, to be decorated
var truck = new Vehicle( "truck" );

// New functionality we're decorating vehicle with
truck.setModel = function( modelName ){
    this.model = modelName;
};

truck.setColor = function( color ){
    this.color = color;
};

// Test the value setters and value assignment works correctly
truck.setModel( "CAT" );
truck.setColor( "blue" );

console.log( truck );

// Outputs:
// vehicle:truck, model:CAT, color: blue

// Demonstrate "vehicle" is still unaltered
var secondInstance = new Vehicle( "car" );
console.log( secondInstance );

// Outputs:
// vehicle: car, model:default, license: 00000-000

Did you find this article valuable?

Support Kushagra Sharma by becoming a sponsor. Any amount is appreciated!