JavaScript Patterns*

*at least some of them...

Patterns definition by GoF

Elements of reusable object-oriented software which describe simple and elegant solutions to specific problems in object-oriented software design.

 

Agenda

  • Constructor
  • Module
  • Revealing Module
  • Singleton
  • Observer
  • Pubsub
  • Mediator
  • Command
  • Fasade
  • Factory
  • Decorator
  • MVC
  • MVP
  • MVVM
  • AMD

Constructor

  • Object constructors are used to create specific types of objects - both preparing the object for use and accepting arguments which a constructor can use to set the values of member properties and methods when the object is first created

Basic constructor

var Car = function(model, year, miles) {
    this.model = model;
    this.year = year;
    this.miles = miles;

    this.toString = function() {
        return this.model + ' has done '  + this.miles + ' miles';
    };
}

// Usage:

// We can create new instances of the car
var civic = new Car('Honda Civic', 2009, 20000);
var mondeo = new Car('Ford Mondeo', 2010, 5000);

// and then open our browser console to view the
// output of the toString() method being called on
// these objects
console.log(civic.toString()); // "Honda Civic has done 20000 miles"
console.log(mondeo.toString()); // "Ford Mondeo has done 5000 miles"

Constructor with prototype

var Car = function(model, year, miles) {
    this.model = model;
    this.year = year;
    this.miles = miles;
}

// Note here that we are using Object.prototype.newMethod rather than
// Object.prototype so as to avoid redefining the prototype object
Car.prototype.toString = function() {
    return this.model + ' has done ' + this.miles + ' miles';
};

// Usage: 
var civic = new Car('Honda Civic', 2009, 20000);
var mondeo = new Car('Ford Mondeo', 2010, 5000);

console.log(civic.toString()); // "Honda Civic has done 20000 miles"
console.log(mondeo.toString()); // "Ford Mondeo has done 5000 miles"

Module

  • used to further emulate the concept of classes in such a way that we're able to include both public/private methods and variables inside a single object, thus shielding particular parts from the global scope

 

  • encapsulates "privacy", state and organization using closures

 

  • results in is a reduction in the likelihood of our function names conflicting with other functions defined in additional scripts on the page

Module template

var myModule = (function() {
    var myPrivateVar, myPrivateMethod;

    // A private variable
    myPrivateVar = 0;

    // A private function which logs any arguments
    myPrivateMethod = function(foo) {
        console.log(foo);
    };

    return {
        // A public variable
        myPublicVar: "foo",
        // A public function utilizing privates
        myPublicFunction: function(bar) {
            // Do smth with our private variable
            myPrivateVar++;
            // Call our private method using bar
            myPrivateMethod(bar);
        }
    };
})();
var counter = (function() {
    var value = 0;

    return {
        increment: function() {
            return value++;
        },
        reset: function() {
            console.log("counter value prior to reset: " + value);
            value = 0;
        }
    };
})();

// Usage:
// Increment our counter
console.log(counter.increment()); // 0
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2

// Reset the counter value
counter.reset(); // 3

// Increment our counter
console.log(counter.increment()); // 0
console.log(counter.increment()); // 1

Example: counter

var $ = 'another cool framework';

// Global module
var myModule = (function($) {
    function privateMethod1() {
        $(".container").html("Hello World!");
    }
    
    function privateMethod2() {
        console.log('called private method');
    }

    return {
        publicMethod: function() {
            privateMethod1();
            // other stuff
        }
    };
    // Pull in jQuery
})(jQuery);

myModule.publicMethod();

Example: module with global dependencies

var myModule = (function(exports) {
    var module1 = exports.module1;
    var module2 = exports.module2;

    var privateMethod = function() {
        console.log('called private method');
        module1.doSomeStuff();
        module2.doSomeStuff();
    }

    return {
        publicMethod: function() {
            privateMethod();
            // other stuff
        }
    };
    // Pull in dependencies
})(exports);

myModule.publicMethod();

Example: module with dependencies 2

// somewhere in other file
var exports = {
    module1: { /* public api goes here */ },
    module2: { /* public api goes here */ }
};
var myRevealingModule = (function () {
        var privateVar = 'Ben Cherry';
        var publicVar = 'Hey there!';
 
        var privateFunction = function() {
            console.log('Name: ' + privateVar);
        }
 
        var publicSetName = function(strName) {
            privateVar = strName;
        }
 
        var publicGetName = function() {
            privateFunction();
        }
 
        // Reveal public pointers to
        // private functions and properties
        return {
            setName: publicSetName,
            greeting: publicVar,
            getName: publicGetName
        };
    })();
 
myRevealingModule.setName('Paul Kinlan');

Revealing module

Singleton

var singleton = (function() {
    // Instance stores a reference to the Singleton
    var instance;

    var init = function() {
        // Private methods and variables
        var randomString = 'I\'m random, believe me!';

        return {
            getRandomString: function() {
                return randomString;
            }
        };
    };

    return {
        // Get the Singleton instance if one exists
        // or create one if it doesn't
        getInstance: function() {
            if (!instance) {
                instance = init();
            }
            
            return instance;
        }
    };
})();

console.log(singleton.getInstance().getRandomString()); // random string
console.log(singleton.getInstance() === singleton.getInstance()); // true

Observer 

  • design pattern where an object (known as a subject) maintains a list of objects depending on it (observers), automatically notifying them of any changes to state

 

  • when a subject needs to notify observers about something interesting happening, it broadcasts a notification to the observers

 

  • particular observer could be removed from subject
var ObserverList = function() {
    this.observerList = [];
}

$.extend(ObserverList.prototype, {
    add: function(obj) {
        return this.observerList.push(obj);
    },
    count: function() {
        return this.observerList.length;
    },
    get: function(index) {
        return this.observerList[index];
    },
    indexOf: function(obj, startIndex) {
        var i = startIndex || 0;

        while (i < this.observerList.length) {
            if (this.observerList[i] === obj) {
                return i;
            }
            
            i++;
        }

        return -1;
    },
    removeAt: function(index) {
        this.observerList.splice(index, 1);
    }
});
var Subject = function() {
    this.observers = new ObserverList();
}

$.extend(Subject.prototype, {
    addObserver: function(observer) {
        this.observers.add(observer);
    },
    removeObserver: function(observer) {
        this.observers.removeAt(this.observers.indexOf(observer, 0));
    },
    notifyPropertyChanged: function(propertyName) {
        var observerCount = this.observers.count(), i;
        for (i = 0; i < observerCount; i++) {
            debugger;
            this.observers.get(i).onSubjectPropertyChanged(this, propertyName);
        }
    }
});
var Observer = function() {
    this.onSubjectPropertyChanged = function(subject, propertyName) {};
};
var subjectCheckbox = document.getElementById('subjectCheckbox');
var addObserverButton = document.getElementById('addNewObserver');
var observersContainer = document.getElementById('observersContainer');

$.extend(subjectCheckbox, new Subject());
subjectCheckbox.onclick = function() {
    this.notifyPropertyChanged('checked');
};

addObserverButton.onclick = function() {
    var observerCheckbox = document.createElement('input');
    observerCheckbox.type = 'checkbox';

    $.extend(observerCheckbox, new Observer());
    observerCheckbox.onSubjectPropertyChanged = function(subject, propertyName) {
        debugger;
        if (propertyName === 'checked') {
            this.checked = subject[propertyName];    
        }
    }
    
    subjectCheckbox.addObserver(observerCheckbox);
    observersContainer.appendChild(observerCheckbox);
};

<button id="addNewObserver">Add New Observer checkbox</button>
<input id="subjectCheckbox" type="checkbox"/>
<div id="observersContainer"></div>

Publish/Subscribe

  • uses a topic/event channel which sits between the objects wishing to receive notifications (subscribers) and the object firing the event (the publisher)

 

  • allows to avoid dependencies between the subscriber and publisher
var pubsub = $('div');
<button id="addNewSubscriber">Add New Subscriber checkbox</button>
<input id="publisherCheckbox1" type="checkbox"/>
<input id="publisherCheckbox2" type="checkbox"/>
<div id="subscribersContainer"></div>
var pubsub = $('div');

var publisherCheckbox1 = document.getElementById('publisherCheckbox1');
var publisherCheckbox2 = document.getElementById('publisherCheckbox2');
var addSubscriberButton = document.getElementById('addNewSubscriber');
var subscribersContainer = document.getElementById('subscribersContainer');
var publisherClickHandler = function () {
    if (this.checked) {
        pubsub.trigger('checked');
    } else {
        pubsub.trigger('unchecked');
    }
};

publisherCheckbox1.onclick = publisherClickHandler;
publisherCheckbox2.onclick = publisherClickHandler;

addSubscriberButton.onclick = function() {
    var subscriberCheckbox = document.createElement('input');
    subscriberCheckbox.type = 'checkbox';
    
    pubsub.on('checked', function () {
        subscriberCheckbox.checked = true;
    });   
    pubsub.on('unchecked', function () {
        subscriberCheckbox.checked = false;
    });
 
    subscribersContainer.appendChild(subscriberCheckbox);
};

Mediator

  • behavioral design pattern that allows us to expose a unified interface through which the different parts of a system may communicate

 

  • promotes loose coupling by ensuring that instead of components referring to each other explicitly, their interaction is handled through this central point
var orgChart = {
 
  addNewEmployee: function(){
 
    // getEmployeeDetail provides a view that users interact with
    var employeeDetail = this.getEmployeeDetail();
 
    // when the employee detail is complete, the mediator (the 'orgchart' object)
    // decides what should happen next
    employeeDetail.on("complete", function(employee){
 
      // set up additional objects that have additional events, which are used
      // by the mediator to do additional things
      var managerSelector = this.selectManager(employee);
      managerSelector.on("save", function(employee){
        employee.save();
      });
 
    });
  },
 
  // ...
}

Command

  • aims to encapsulate method invocation, requests or operations into a single object and gives us the ability to both parameterize and pass method calls around that can be executed at our discretion
  • enables us to decouple objects invoking the action from the objects which implement them, giving us a greater degree of overall flexibility in swapping out concrete classes(objects).
var Command = function(target, methodName, var_args) {
    // get target method and validate it
    var method = target && target[methodName];
    if (!method || typeof method !== 'function') {
        throw 'Target object should contain method: "' + methodName + '"';
    }

    this.target = target;
    this.method = method;

    // check any predefined args
    if (var_args) {
        this.args = [].splice.call(arguments, 2);
    } else {
        this.args = [];
    }
};

Command.prototype.execute = function() {
    return this.method.apply(this.target, this.args);
}
var diceRollView = (function() {
    var diceElement = document.getElementById('dice');

    return {
        generateNumber: function(from, to) {
            diceElement.innerText = Math.round(Math.random() * (to - from) + from);
        }
    }
}());

// encapsulate dice roll login in a single command and pass it to the view
var rollTheDiceCommand = new Command(diceRollView, 'generateNumber', 1, 6);

var userInteractionView = (function(rollCommand) {
    var rollButton = document.getElementById('roll');

    return {
        init: function() {
            rollButton.onclick = function() {
                rollCommand.execute();
            }
        }
    }
}(rollTheDiceCommand));

userInteractionView.init();
<button id="roll">Roll the dice!</button>
<div id="dice"></div>

Facade

  • provides a convenient higher-level interface to a larger body of code, hiding its true underlying complexity

 

  • simplifies the API being presented to other developers, something which almost always improves usability
var addEvent = function(element, eventName, handler) {
    if (element.addEventListener) {
        element.addEventListener(eventName, handler, false);
    } else if (element.attachEvent) {
        element.attachEvent("on" + eventName, handler);
    } else {
        element["on" + eventName] = handler;
    }
};

Factory

  • provides a generic interface for creating objects

 

  • useful if the object creation process is relatively complex, e.g. if it strongly depends on dynamic factors or application configuration
var WebSocketTransport = function () {
    // websocket implementation details
};
var AjaxTransport = function () {
    // long polling/polling implementation details
};
var StubbedTransport = function () {
    // some test data implementation
};

var environment = 'test';
var realtimeTransportFactory = (function () {
    return {
        createTransport: function (options) {
            if (environment === 'test') {
                return new StubbedTransport(options);
            } else if (window.WebSocket) {
                return new WebSocketTransport(options);
            } else {
                return new AjaxTransport(options);
            }
        }
    };
}());

var transport = realtimeTransportFactory.createTransport();
console.log(transport instanceof StubbedTransport); // true

environment = 'prod';
transport = realtimeTransportFactory.createTransport();
console.log(transport instanceof WebSocketTransport); // true

window.WebSocket = null;
transport = realtimeTransportFactory.createTransport(); 
console.log(transport instanceof AjaxTransport); // true

Decorator

  • offeres the ability to add behaviour to existing classes in a system dynamically

 

  • used to modify existing systems where we wish to add additional features to objects without the need to heavily modify the underlying code using them
var Clock = function () {};
Clock.prototype.getTime = function () {
    return new Date().getTime();  
};
// logs each call to methods of underlying object
var LoggingDecorator = function (target) {
    this.target = target;
}
LoggingDecorator.prototype.getTime = function () {
    var time = this.target.getTime();
    console.log('getTime called, return value:', time);
    return time;
};
// caches return values from underlying object
var CachingDecorator = function (target) {
    this.target = target;
}
CachingDecorator.prototype.getTime = function () {
    if (!this.cachedTime) {
        this.cachedTime = this.target.getTime();
    }
    
    return this.cachedTime;
};

var clock = new Clock();
var decoratedClock = new CachingDecorator(new LoggingDecorator(clock)); // xzibit is happy
var interval = setInterval(function () {
    console.log('clock value', clock.getTime());
    console.log('decorated clock value', decoratedClock.getTime());
;}, 1000);
setTimeout(function () {
    clearInterval(interval);
}, 2500);

MVC

  • architectural design pattern that encourages improved application organization through a separation of concerns

 

  • enforces the isolation of business data (Models) from user interfaces (Views), with a third component (Controllers) traditionally managing logic and user-input

Model

  • manages data
  • contains business logic
  • works with ajax, local storage etc.
  • Subject for the View

View

  • visualizes data
  • collects user input
  • notifies controller about user actions
  • works with DOM
  • Observer for the Model

Controller

  • manages flow triggered by user actions
  • updates model

MVP

  • derivative of the MVC design pattern which focuses on improving presentation logic

 

  • view containing little to no logic in compare to MVC

 

  • presenter is used to bind model to view

Model

  • manages data
  • contains business logic
  • works with ajax, local storage etc.
  • may be Subject for the Presenter

View

  • visualizes data
  • collects user input
  • works with DOM
  • exposes some interface for a presenter (notify about actions, render etc.)

Presenter

  • manages flow triggered by user actions
  • updates model
  • Observer for the Model
  • prepares data for the View

MVVM

  • an architectural pattern based on MVC and MVP, which attempts to more clearly separate the development of user-interfaces (UI) from that of the business logic and behavior in an application

 

  • ViewModel can be considered a specialized Controller that acts as a data converter. It changes Model information into View information, passing commands from the View to the Model

Model

  • manages data
  • contains business logic
  • works with ajax, local storage etc.
  • may be Subject for the Presenter

View

  • visualizes data
  • uses some unified declarative bindings  to ViewModel

ViewModel

  • exposes Commands for user actions
  • acts as a data converter for the view

MV*

What does MV* give us?

  • Easier overall maintenance. When updates need to be made to the application it is very clear whether the changes are data-centric, meaning changes to models and possibly controllers, or merely visual, meaning changes to views.

 

  • Decoupling models and views means that it is significantly more straight-forward to write unit tests for business logic

 

  • Duplication of low-level model and controller code (i.e what we may have been using instead) is eliminated across the application

 

  • Depending on the size of the application and separation of roles, this modularity allows developers responsible for core logic and developers working on the user-interfaces to work simultaneously

AMD

  • proposal for defining modules where both the module and dependencies can be asynchronously loaded

 

  • allows not to care about scripts including sequence

 

  • allows to load scripts exactly on page that requires them
// module1.js
define('module1', function () {
    return 'module1';
});
<script data-main="js/init" src="js/library/require.js"> < / script >
// module2.js
define('module2', function () {
    return 'module2';
});
// module3.js
define('module3', ['module1', 'module2'], function (module1, module2) {
    return ['module3', module1, module2].join(' + ');
});
// app entry point
// init.js
require('module3', function (module3) {
    console.log(module3);
    // outputs: module3 + module1 + module2
});

requirejs

?

JS Patterns

By Max Bartoshik