JavaScript Design Patterns - The Module Pattern
Join the DZone community and get the full member experience.
Join For FreeAs I mentioned a few days ago, I'm currently in the process of reading Addy Osmani's JavaScript Design Patterns book. (Note - in my earlier blog entry I linked to the physical copy at Amazon. The link here is to the free online version. I think his book is worth buying personally, but you can try before you buy!) The first pattern described in the book, and the one I'm going to talk about today, is the Module pattern.
Before I begin, I want to be clear. I'm writing this as a means to help me cement my understanding. I am not an expert. I'm learning. I expect to make mistakes, and I expect my readers to call me out on it. If in the end we all learn something together, then I think this process is worth while!
Addy describes the Module pattern like so:
The Module pattern was originally defined as a way to provide both private and public encapsulation for classes in conventional software engineering.In JavaScript, the Module pattern is 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. What this results in is a reduction in the likelihood of our function names conflicting with other functions defined in additional scripts on the page.
I think this makes sense by itself, but it may help to back up a bit and consider why encapsulation may be good in general. If you are new to JavaScript, you have probably built web pages that look a bit like this.
<html> <head> <title></title> <script> //stuff here </script> </head> <body> <!--awesome layout here --> </body> </html>
You may begin adding interactivity to your page by adding a simple JavaScript function:
<html> <head> <title></title> <script> function doSomething() { } </script> </head> <body> <!--awesome layout here --> </body> </html>
And then progressively adding more and more...
<html> <head> <title></title> <script> function doSomething() { } function doSomethingElse() { } function heyINeedThisToo() { } </script> </head> <body> <!--awesome layout here --> </body> </html>
Eventually, the amount of JavaScript code you have may get to a point where you realize you probably should be storing it in its own file. But moving all of that code into a separate file doesn't necessarily help. After a while you begin finding yourself having difficulty finding the right functions. You may have code related to feature X next to feature Y followed by something else related to feature X. Maybe than you create multiple JavaScript files. One for X, one for Y. But as Addy alludes to above, you run the risk of function names overwriting each other.
Consider a web application that interfaces with both Facebook and Twitter. You write a function for Twitter called getLatest that fetches the latest Tweets about Paris Hilton. (And why wouldn't you do that?) You then write code to get the latest status updates from your friends on Facebook. What should we call that? Oh yeah - getLatest!
This isn't something you couldn't handle, of course, but the point is, design patterns were built for the sole purpose of helping you solve problems. In this case, the module pattern is one example of a solution that helps you organize your code. Let's look at a full example of this.
I've created a simple web application that lets users keep a diary. For now the functionality is simple - you can see your past entries, write new ones, and view a particular entry. This web application is going to make use of WebSQL, which does not work in Firefox or IE! This is intentional and I plan to address this in a later blog entry. You can demo this (again, please use Chrome, and again, keep in mind there is a reason why I'm not handling multiple browsers) by going here:
http://www.raymondcamden.com/demos/2013/mar/22/v1/
While you can view source yourself, I thought it might be beneficial to share the main JavaScript file here. Warning, this is a big code block. That's kind of the point. Do not read every line here.
var db; var mainView; $(document).ready(function() { //create a new instance of our Diary and listen for it to complete it's setup setupDiary(startApp); }); function dbErrorHandler(e) { console.log('DB Error'); console.dir(e); } function setupDiary(callback) { //First, setup the database db = window.openDatabase("diary", 1, "diary", 1000000); db.transaction(initDB, dbErrorHandler, callback); } function initDB(t) { t.executeSql('create table if not exists diary(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT, image TEXT, published DATE)'); } /* Main application handler. At this point my database is setup and I can start listening for events. */ function startApp() { console.log('startApp'); mainView = $("#mainView"); //Load the main view pageLoad("main.html"); //Always listen for home click $(document).on("touchend", ".homeButton", function(e) { e.preventDefault(); pageLoad("main.html"); }); } function pageLoad(u) { console.log("load "+u); //convert url params into an ob var data = {}; if(u.indexOf("?") >= 0) { var qs = u.split("?")[1]; var parts = qs.split("&"); for(var i=0, len=parts.length; i<len; i++) { var bits = parts[i].split("="); data[bits[0]] = bits[1]; }; } $.get(u,function(res,code) { mainView.html(res); var evt = document.createEvent('CustomEvent'); evt.initCustomEvent("pageload",true,true,data); var page = $("div", mainView); page[0].dispatchEvent(evt); }); } //Utility to convert record sets into array of obs function fixResults(res) { var result = []; for(var i=0, len=res.rows.length; i<len; i++) { var row = res.rows.item(i); result.push(row); } return result; } function getDiaryEntries(start,callback) { console.log('Running getEntries'); if(arguments.length === 1) callback = arguments[0]; db.transaction( function(t) { t.executeSql('select id, title, body, image, published from diary order by published desc',[], function(t,results) { callback(fixResults(results)); },dbErrorHandler); }, dbErrorHandler); } $(document).on("pageload", "#mainPage", function(e) { getDiaryEntries(function(data) { console.log('getEntries'); var s = ""; for(var i=0, len=data.length; i<len; i++) { s += "<div data-id='"+data[i].id+"'>" + data[i].title + "</div>"; } $("#entryList").html(s); //Listen for add clicks $("#addEntryBtn").on("touchend", function(e) { e.preventDefault(); pageLoad("add.html"); }); //Listen for entry clicks $("#entryList div").on("touchend", function(e) { e.preventDefault(); var id = $(this).data("id"); pageLoad("entry.html?id="+id); }); }); }); function fixResult(res) { if(res.rows.length) { return res.rows.item(0); } else return {}; } function getEntry(id, callback) { db.transaction( function(t) { t.executeSql('select id, title, body, image, published from diary where id = ?', [id], function(t, results) { callback(fixResult(results)); }, dbErrorHandler); }, dbErrorHandler); } $(document).on("pageload", "#entryPage", function(e) { getEntry(Number(e.detail.id), function(ob) { var content = "<h2>" + ob.title + "</h2>"; content += "Written "+dtFormat(ob.published) + "<br/><br/>"; content += ob.body; $("#entryDisplay").html(content); }); }); function saveEntry(data, callback) { db.transaction( function(t) { t.executeSql('insert into diary(title,body,published) values(?,?,?)', [data.title, data.body, new Date().getTime()], function() { callback(); }, dbErrorHandler); }, dbErrorHandler); } $(document).on("pageload", "#addPage", function(e) { $("#addEntrySubmit").on("touchstart", function(e) { e.preventDefault(); //grab the values var title = $("#entryTitle").val(); var body = $("#entryBody").val(); //store! saveEntry({title:title,body:body}, function() { pageLoad("main.html"); }); }); }); function dtFormat(input) { if(!input) return ""; input = new Date(input); var res = (input.getMonth()+1) + "/" + input.getDate() + "/" + input.getFullYear() + " "; var hour = input.getHours()+1; var ampm = "AM"; if(hour === 12) ampm = "PM"; if(hour > 12){ hour-=12; ampm = "PM"; } var minute = input.getMinutes()+1; if(minute < 10) minute = "0" + minute; res += hour + ":" + minute + " " + ampm; return res; }
What you see is how I typically code. My application begins by waiting for the DOM to load. It then needs to setup a database. Next, it needs to list them out. And so on. If you read "down" the JavaScript file, you can see functions related to DOM manipulation, functions handling my "single page architecture", and functions doing database crap. When I was working on page one, I focused on lists. When I worked on the add form, I worked on data writing support. Basically, as I moved into the application I just plopped down functions one after another.
This works. But - my gut is telling me that this file is messy. When I see something wrong, I'm not sure where to look since everything is mixed up together. In the paragraph above I described most of my functionality as falling into three main areas. I've decided that I'd like to take database stuff and abstract it into a module.
The basic structure of code using the Module pattern can be defined like so:
var someModule = (function() { }());
Raise your hand if you find that syntax confusing. I know I did. Now it makes sense to me. But for a long time it just felt... weird. I had a mental block accepting the form of this code for a long time. I'm not ashamed to admit it.
For me, it helped if I backed up a bit and looked at it like so:
var someModule = ( );
Ok, that makes sense. I'm basically saying the variable someModule is equal to whatever in the heck gets done inside my parentheses. Ok, so what in the heck is happening there...
function() { }()
Ok, so we're creating a function and running it. Immediately. Ok, so whatever the function returns - that's what will be passed to someModule. Here is where things get interesting. Let's put some code in this module.
var someModule = (function() { //Credit Addy Osmani: http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript var counter = 0; return { incrementCounter: function () { return counter++; }, resetCounter: function () { console.log( "counter value prior to reset: " + counter ); counter = 0; } }; }());
The code for this module comes from Addy's example. The variable, counter, is a private variable. Why? Because it isn't actually returned from the function. Instead, we return an object literal that contains two functions. These functions have access to the variable counter, because, at creation, they were in the same scope. (Note: This last sentence may not be precisely describing the situation.) The result of this function is that object literal. My module contains two functions and a private data variable.
What's nice is - I no longer have to worry about function collision. I can name my functions whatever I darn well please. I can also create private functions as well and hide them away from the implementation. There may be utility functions that make sense for my module but don't make sense anywhere else. I can stuff them in there and hide them away!
So, as I said, I wanted to take out the database aspects of my code. I created a new file, diary.js, and created this module.
var diaryModule = (function() { var db; function initDB(t) { t.executeSql('create table if not exists diary(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT, image TEXT, published DATE)'); } function dbErrorHandler(e) { console.log('DB Error'); console.dir(e); } //Utility to convert record sets into array of obs function fixResults(res) { var result = []; for(var i=0, len=res.rows.length; i<len; i++) { var row = res.rows.item(i); result.push(row); } return result; } //I'm a lot like fixResults, but I'm only used in the context of expecting one row, so I return an ob, not an array function fixResult(res) { if(res.rows.length) { return res.rows.item(0); } else return {}; } return { setup:function(callback) { db = window.openDatabase("diary", 1, "diary", 1000000); db.transaction(initDB, dbErrorHandler, callback); }, getEntries:function(start,callback) { console.log('Running getEntries'); if(arguments.length === 1) callback = arguments[0]; db.transaction( function(t) { t.executeSql('select id, title, body, image, published from diary order by published desc',[], function(t,results) { callback(fixResults(results)); },dbErrorHandler); }, dbErrorHandler); }, getEntry:function(id, callback) { db.transaction( function(t) { t.executeSql('select id, title, body, image, published from diary where id = ?', [id], function(t, results) { callback(fixResult(results)); }, dbErrorHandler); }, dbErrorHandler); }, saveEntry:function(data, callback) { db.transaction( function(t) { t.executeSql('insert into diary(title,body,published) values(?,?,?)', [data.title, data.body, new Date().getTime()], function() { callback(); }, dbErrorHandler); }, dbErrorHandler); } } }());
Now I've got a "package" that my main JavaScript file can use. Let's look at that.
var mainView; $(document).ready(function() { //create a new instance of our Diary and listen for it to complete it's setup diaryModule.setup(startApp); }); /* Main application handler. At this point my database is setup and I can start listening for events. */ function startApp() { console.log('startApp'); mainView = $("#mainView"); //Load the main view pageLoad("main.html"); //Always listen for home click $(document).on("touchend", ".homeButton", function(e) { e.preventDefault(); pageLoad("main.html"); }); } function pageLoad(u) { console.log("load "+u); //convert url params into an ob var data = {}; if(u.indexOf("?") >= 0) { var qs = u.split("?")[1]; var parts = qs.split("&"); for(var i=0, len=parts.length; i<len; i++) { var bits = parts[i].split("="); data[bits[0]] = bits[1]; }; } $.get(u,function(res,code) { mainView.html(res); var evt = document.createEvent('CustomEvent'); evt.initCustomEvent("pageload",true,true,data); var page = $("div", mainView); page[0].dispatchEvent(evt); }); } $(document).on("pageload", "#mainPage", function(e) { diaryModule.getEntries(function(data) { console.log('getEntries'); var s = ""; for(var i=0, len=data.length; i<len; i++) { s += "<div data-id='"+data[i].id+"'>" + data[i].title + "</div>"; } $("#entryList").html(s); //Listen for add clicks $("#addEntryBtn").on("touchend", function(e) { e.preventDefault(); pageLoad("add.html"); }); //Listen for entry clicks $("#entryList div").on("touchend", function(e) { e.preventDefault(); var id = $(this).data("id"); pageLoad("entry.html?id="+id); }); }); }); $(document).on("pageload", "#entryPage", function(e) { diaryModule.getEntry(Number(e.detail.id), function(ob) { var content = "<h2>" + ob.title + "</h2>"; content += "Written "+dtFormat(ob.published) + "<br/><br/>"; content += ob.body; $("#entryDisplay").html(content); }); }); $(document).on("pageload", "#addPage", function(e) { $("#addEntrySubmit").on("touchstart", function(e) { e.preventDefault(); //grab the values var title = $("#entryTitle").val(); var body = $("#entryBody").val(); //store! diaryModule.saveEntry({title:title,body:body}, function() { pageLoad("main.html"); }); }); }); function dtFormat(input) { if(!input) return ""; input = new Date(input); var res = (input.getMonth()+1) + "/" + input.getDate() + "/" + input.getFullYear() + " "; var hour = input.getHours()+1; var ampm = "AM"; if(hour === 12) ampm = "PM"; if(hour > 12){ hour-=12; ampm = "PM"; } var minute = input.getMinutes()+1; if(minute < 10) minute = "0" + minute; res += hour + ":" + minute + " " + ampm; return res; }
You can see that all of the database functions are now gone. I now access them via diaryModule.whatever. I believe I cut out about 60 or so lines of code, around a third, but what is left is more focused. (I could also take out the functions used to support my single page architecture.)
You can run this version here: http://www.raymondcamden.com/demos/2013/mar/22/v2
So, questions? Opinions? Rants? :)
Published at DZone with permission of Raymond Camden, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments