Revealing Module Pattern: Structuring JavaScript Code – Part III
- select the contributor at the end of the page -
Using the Revealing Module Pattern
This is the 3rd post in a series on techniques, strategies and patterns for writing JavaScript code.The Prototype Pattern shown in an earlier post works well and is quite efficient, but it's not the only game in town. One of my favorite overall JavaScript patterns is the Revealing Module Pattern since it's cleaner with less usage of the “this” keyword. I also like the fact that it doesn't separate code into constructor and prototype sections. Although it doesn't offer the benefit of sharing functions implementations across objects through JavaScript's prototype feature, it's definitely a viable option.
As a quick review, I showed the following code in the first post since it shows “function spaghetti code” and illustrates code that can be encapsulated into a more re-useable object. The code simply lists all functions directly with no encapsulation and defines several global variables. While the code works fine this way, I'll examine how we can restructure it to follow the Revealing Module Pattern.
window.onload = function () {eqCtl = document.getElementById('eq');
currNumberCtl = document.getElementById('currNumber');
};
var eqCtl,
currNumberCtl,
operator,
operatorSet = false,
equalsPressed = false,
lastNumber = null;
function add(x,y) {
return x + y;
}
function subtract(x, y) {
return x - y;
}
function multiply(x, y) {
return x * y;
}
function divide(x, y) {
if (y == 0) {
alert("Can't divide by 0");
return 0;
}
return x / y;
}
function setVal(val) {
currNumberCtl.innerHTML = val;
}
function setEquation(val) {
eqCtl.innerHTML = val;
}
function clearNumbers() {
lastNumber = null;
equalsPressed = operatorSet = false;
setVal('0');
setEquation('');
}
function setOperator(newOperator) {
if (newOperator == '=') {
equalsPressed = true;
calculate();
setEquation('');
return;
}
//Handle case where = was pressed
//followed by an operator (+, -, *, /)
if (!equalsPressed) calculate();
equalsPressed = false;
operator = newOperator;
operatorSet = true;
lastNumber = parseFloat(currNumberCtl.innerHTML);
var eqText = (eqCtl.innerHTML == '') ?
lastNumber + ' ' + operator + ' ' :
eqCtl.innerHTML + ' ' + operator + ' ';
setEquation(eqText);
}
function numberClick(e) {
var button = (e.target) ? e.target : e.srcElement;
if (operatorSet == true || currNumberCtl.innerHTML == '0') {
setVal('');
operatorSet = false;
}
setVal(currNumberCtl.innerHTML + button.innerHTML);
setEquation(eqCtl.innerHTML + button.innerHTML);
}
function calculate() {
if (!operator || lastNumber == null) return;
var currNumber = parseFloat(currNumberCtl.innerHTML),
newVal = 0;
//eval() would've made this a whole lot simpler
//but didn't want to use it in favor of a more
//"robust" set of methods to demo patterns
switch (operator) {
case '+':
newVal = add(lastNumber, currNumber);
break;
case '-':
newVal = subtract(lastNumber, currNumber);
break;
case '*':
newVal = multiply(lastNumber, currNumber);
break;
case '/':
newVal = divide(lastNumber, currNumber);
break;
}
setVal(newVal);
lastNumber = newVal;
}
The Revealing Module Pattern is based on a pattern referred to as the Module Pattern. It makes reading code easier (in my opinion anyway) and allows it to be organized in a more structured manner. The pattern starts with code like the following to define a variable, associate it with a function and then invoke the function immediately as the script loads. The final parenthesis shown in the code cause it to be invoked.
var calculator = function () { /* Code goes here */ }();
Variables and functions that should be encapsulated within the calculator object go in the “Code goes here” section. What's really nice about the pattern is that you can define which members are publicly accessible and which members are private. This is done by adding a return statement at the end of the function that exposes the public members. The following code demonstrates how the calculator functionality can be refactored to follow the Revealing Module Pattern:
var calculator = function () {var eqCtl,
currNumberCtl,
operator,
operatorSet = false,
equalsPressed = false,
lastNumber = null,
init = function (equals, currNumber) {
eqCtl = equals;
currNumberCtl = currNumber;
},
add = function (x, y) {
return x + y;
},
subtract = function (x, y) {
return x - y;
},
multiply = function (x, y) {
return x * y;
},
divide = function (x, y) {
if (y == 0) {
alert("Can't divide by 0");
return 0;
}
return x / y;
},
setVal = function (val) {
currNumberCtl.innerHTML = val;
},
setEquation = function(val) {
eqCtl.innerHTML = val;
},
clearNumbers = function() {
lastNumber = null;
equalsPressed = operatorSet = false;
setVal('0');
setEquation('');
},
setOperator = function(newOperator) {
if (newOperator == '=') {
equalsPressed = true;
calculate();
setEquation('');
return;
}
//Handle case where = was pressed
//followed by an operator (+, -, *, /)
if (!equalsPressed) calculate();
equalsPressed = false;
operator = newOperator;
operatorSet = true;
lastNumber = parseFloat(currNumberCtl.innerHTML);
var eqText = (eqCtl.innerHTML == '') ?
lastNumber + ' ' + operator + ' ' :
eqCtl.innerHTML + ' ' + operator + ' ';
setEquation(eqText);
},
numberClick = function(e) {
var button = (e.target) ? e.target : e.srcElement;
if (operatorSet == true ||
currNumberCtl.innerHTML == '0') {
setVal('');
operatorSet = false;
}
setVal(currNumberCtl.innerHTML + button.innerHTML);
setEquation(eqCtl.innerHTML + button.innerHTML);
},
calculate = function() {
if (!operator || lastNumber == null) return;
var currNumber = parseFloat(currNumberCtl.innerHTML),
newVal = 0;
//eval() would've made this a whole lot simpler
//but didn't want to use it in favor of a more
//"robust" set of methods to demo patterns
switch (operator) {
case '+':
newVal = add(lastNumber, currNumber);
break;
case '-':
newVal = subtract(lastNumber, currNumber);
break;
case '*':
newVal = multiply(lastNumber, currNumber);
break;
case '/':
newVal = divide(lastNumber, currNumber);
break;
}
setVal(newVal);
lastNumber = newVal;
};
return {
init: init,
numberClick: numberClick,
setOperator: setOperator,
clearNumbers: clearNumbers
};
} ();
Functions that should be exposed publicly are defined in the return section of the calculator object. In this example the init, numberClick, setOperator and clearNumbers functions are exposed by simply defining a JavaScript object literal that is returned when the main calculator function is invoked. All of the other functions and variables defined in the calculator object are private. JavaScript doesn't support accessibility modifiers as C# or Java do but this pattern provides a nice way to emulate that type of functionality.
Looking through the code you may notice that a new function named init() was added that wasn't in the previous examples. It's responsible for accepting any initialization data that the calculator object needs to work correctly. As soon as the page loads the calculator object is created but init() needs to be called to pass two HTML elements that it interacts with. An example of calling init() is shown next:
window.onload = function () {var eqCtl = document.getElementById('eq');
var currNumberCtl = document.getElementById('currNumber');
calculator.init(eqCtl, currNumberCtl);
};
The Revealing Module Pattern is currently my favorite pattern out there for structuring JavaScript code mainly because it's easy to use, very readable (which is important for maintenance), and provides a simple way to expose public members to consumers. What if we could combine this pattern with the Prototype Pattern though to get the benefits provided by prototyping? That'll be the subject of my next post.
Creating Multiple Object Instances
When the calculator code shown previously is initially parsed the function assigned to the calculator variable is invoked right away which creates one object in memory. This is done by adding parenthesis immediately after the function definition. But, what if you'd like to create and use multiple calculator objects on a single page?
There's another technique that can be used with the Revealing Module Pattern if/when multiple objects need to be created in a page or script. This is done by removing the final parenthesis in the object's code. Note that it's considered a best practice to name the object Calculator instead of calculator (notice the first letter is upper-cased) when it needs to be invoked using the “new” keyword. An example of doing this and removing the final parenthesis is shown next:
var Calculator = function () {var eqCtl,
currNumberCtl,
operator,
operatorSet = false,
equalsPressed = false,
lastNumber = null,
//Functions go here
return {
init: init,
numberClick: numberClick,
setOperator: setOperator,
clearNumbers: clearNumbers
};
};
To create a new instance of the Calculator you can use the following code:
var myCalc;window.onload = function () {
var eqCtl = document.getElementById('eq');
var currNumberCtl = document.getElementById('currNumber');
myCalc = new Calculator(); //Invoke the object
myCalc.init(eqCtl, currNumberCtl);
};
Once the Calculator function is invoked and assigned to the myCalc variable, the public functions exposed by Calculator can be called through myCalc. For example, the previous code uses myCalc to call the Calculator object's init() function.
Following this technique, multiple objects can be created as needed in a page or script. Keep in mind that each call to Calculator() places a new copy of each function in memory, but the impact is quite minimal in this case. If you're worried about multiple copies of functions being placed in memory as objects are created then consider using the Revealing Prototype Pattern since it leverages JavaScript prototyping. I'll cover this pattern later in this series.
Demos of all the patterns covered in this series can be downloaded below.
Download Code
This post covers patterns and techniques found in Dan Wahlin's Structuring JavaScript Code course on Pluralsight.com. If you're on Twitter follow Dan at @DanWahlin.
Ready to test your skills in JavaScript? See how they stack up with this assessment from Smarterer. Start this JavaScript test now