Thursday, September 24, 2009

Javascript Design Patterns

JavaScript Design Patterns

Intent: to convey the meaning of the most commonly used javascript design patterns.
Level: Intermediate to Advanced

Who should read this?
Everyone who touches with javascript, no matter the seniority.


Naming conventions used below:
I like to use modified Hungarian Naming Convention for my variables in loosely-typed languages like PHP or JavaScript. This convention uses first letter of the variable to denote the variable type: sName - string, nCount - numeric, fCallback - function, bIsAwesome - boolean, mParam - mixed (more than one type can be expected).

JavaScript Library used in the examples:
For the following examples I use PrototypeJS library. Documentation is readily available on the web.



Namespace Pattern




Namespaces are used to confine multiple objects and methods to a scope to prevent their cross-interference and for organizational purposes. It is also a syntax sugar for those appreciating order and structure.

var MYAPP = {};
MYAPP.MyModule = function() { ... };

Singleton Pattern




Singleton pattern is used to create a singular object instance and restrict all further instantiations. It is useful when an object that has unique meaning in the scope of the application and no two (or more) objects of the kind can exist.

Example: User Agent, JavaScript Page Controller, Logged-in user, etc.

There are various ways to implement singleton in JavaScript:
  1. The Object Literal Singleton:
    var s = {
    property: 'value'
    };

    PROS: Ease, Compliant with Prototype's extension model
    CONS: No private encapsulation, Have to maintain comma-separation

  2. The Closure Singleton:

    var s = function {
    var private_property = 'value1';
    return {
    public_property: 'value2'
    }
    }();

    PROS: Encapsulation of private members
    CONS: Readability - must see the end parentheses of potentially long declaration to understand the pattern; not compliant with Prototype's extension model

  3. The Constructor Singleton:

    var s = new function {
    var private_property = 'value1';
    this.public_property = 'value2';
    };
    PROS: Encapsulation of private members, Readability - the 'new' keyword on the first line is easy to understand.
    CONS: Not compliant with Prototype's extension model.

After evaluation of pros and cons you can see that a case of extending an object to create a singleton is rare to non-existent, so the third pattern has virtually no cons.

Registerer Pattern




Registerer pattern is used to attach behavior to a group of similar controls on the page, who's markup was created on the server side. Note, it may also be conjoined with singleton pattern as the following example:

var Registerer = new Function() {
this.registerInputExample1 = function(sElementId) {
var oElement = $(sElementId); // get dom object
oElement.observe('click', function() {
// handle click occurred on oElement
});
oElement.observe('mouseover', function() {
// handle mouseover occurred on oElement
});
};

this.registerOutputExample2 = function(sElementId) {
var oElement = $(sElement); // get dom object
document.observe('registerer:somecustomevent', function() {
oElement.update('received an event'); // handle how custom event affects the element.
});
}
}



Output Example: Imagine a progress bar that must be updated while user rates images. When user rates 100 images using five-star rating widget (described further) The progress bar would show 100% completion.

Input Example: Imagine a five star rating widget that will fire event whenever one of the stars is clicked.

The two widgets are independent from one another and can be rendered multiple times across the page. Both will follow registerer pattern.

Output Example:

var ProgressBar = new Function() {
this.registerOutputBar = function(sElementId) {
var oElement = $(sElementId);
document.observe('progressbar:setpercent', function(oEvent) {
nParentWidth = oElement.parentNode.getWidth();
nWidth = Math.round(nParentWidth / 100 * oEvent.memo.percent);
oElement.setStyle({width: nWidth + 'px'});
});
}
};

Input Example:

var FiveStarRater = new Function() {
/**
* @param sElementId (String) Id of a clickable star
* @param nImageId (Number) Database id of image to rate
* @param nRating (Number) An integer 1-5 to signify rating
*/
this.registerInputStar = function(sElementId, nImageId, nRating) {
var oElement = $(sElementId);
oElement.observe('click', function() {
Server.updateImageRating(nImageId, nRating); // update image rating via ajax
});
}
};


This way, you can apply behaviors to numerous inputs and outputs on the page without having to duplicate functionality for each instance.

PROBLEM:
What if a single star rater is responsible for rating a stream of images? Do we need to register the star for each image? No. The Getter Parameter Pattern comes to the resque.


Getter Parameter Pattern





Consider the problem described in the Registerer Pattern section. The solution is very simple: supply a getter function as a parameter instead of a static image id. The modified code of a registerer will look like this:

var FiveStarRater = new Function() {
/**
* @param sElementId (String) Id of a clickable star
* @param nImageId (Number) Database id of image to rate
* @param nRating (Number) An integer 1-5 to signify rating
*/
this.registerInputStar = function(sElementId, fGetImageId, nRating) {
var oElement = $(sElementId);
oElement.observe('click', function() {
var nImageId = fGetImageId();
Server.updateImageRating(nImageId, nRating); // update image rating via ajax
});
}
};


The getter would be a zero parameter function that in the context of a situation knows how to retrieve a single value:

var nImageId = 234;
function getImageId() {
return nImageId;
}

// Register the elements
FiveStarRater.registerInputStar('star-one', getImageId, 1);
...
FiveStarRater.registerInputStar('star-five', getImageId, 5);


This makes our FiveStarRater context-independent: the knowledge of how to retrieve a context-dependent value (such as nImageId) is now delegated to the caller scope.

No comments:

Post a Comment