My descent into AMD…

Asynchronous module definition. It almost sounds like something out of science fiction. What is this relatively newfangled concept, and why shouldn’t you just hike your pants up to your nipples and tell it to stay off your lawn? I, myself, had to be dragged into it kicking and screaming, and now that I’m familiar with it, I can’t imagine how we didn’t come up with the concept even sooner. Simply put, AMD is a methodology that allows us to implement small, disparate code modules to facilitate better usage of the Single responsibility principle.

The Philosophy

Let’s examine a snippet of code to look at why SRP and AMD are both good things in the world of javascript development.

var balloons = [],
    i, j, k, l;

balloons.push({
    color: 'red',
    fillPercent: 0,
    popped: false
});

balloons.push({
    color: 'green',
    fillPercent: 0,
    popped: false
});

balloons.push({
    color: 'blue',
    fillPercent: 0,
    popped: false
});

function inflate(balloon) {
    if (!balloon.popped) {
        balloon.fillPercent += 5;
        console.log('inflating ' + balloon.color + ' balloon...');

        if (balloon.fillPercent > 100) {
            pop(balloon);
        }
    }
}

function pop(balloon) {
    balloon.fillPercent = 0;
    balloon.popped = true;
    console.log(balloon.color + ' balloon just popped!');
}

for (i = 0, j = balloons.length; i < j; i++) {
    l = Math.floor(Math.random() * 30) + 1;
    for (k = 0; k < l; k++) {
        inflate(balloons[i]);
    }
}

So, this is a fairly simple snippet of code. We create 3 new objects to store balloon data in an array of balloons, define a couple of functions with some console.log() statements for output, and we randomly inflate them anywhere from 1 to 30 times apiece. If they exceed 100% filled, they pop. This code is hideous. It’s spaghetti. There’s no way to reuse the bulk of it. The only thing it has going for it is that we at least made functions for inflate() and pop(), so we could reuse that bit of functionality. Let’s start by creating a constructor function to handle the balloons themselves.

function Balloon(color) {
    this.color = color;
    this.fillPercent = 0;
    this.popped = false;
}

We can also incorporate those 2 previously created functions into the balloon’s .prototype property for use in all of our balloon object instances.

Balloon.prototype = {
    inflate: function() {
        if (!this.popped) {
            this.fillPercent += 5;
            console.log('inflating ' + this.color + ' balloon...');

            if (this.fillPercent > 100) {
                this.pop();
            }
        }
    },

    pop: function() {
        this.fillPercent = 0;
        this.popped = true;
        console.log(this.color + ' balloon just popped!');
    }
};

Now we can execute our previous code with only minor changes:

var balloons = [];
    i, j, k, l;

balloons.push(new Balloon('red'));
balloons.push(new Balloon('green'));
balloons.push(new Balloon('blue'));

for (i = 0, j = balloons.length; i < j; i++) {
    l = Math.floor(Math.random() * 30) + 1;
    for (k = 0; k < l; k++) {
        balloons[i].inflate();
    }
}

So, this is starting to look a lot better now. However, we can go ahead and break the boilerplate code of class constructor and prototype out into a new code module at this point, which we can include in any other page or file where we may need balloon functionality. This is similar to just breaking it out into its own file, e.g. balloon.js, and including that with a script tag, except that with this older method, we run the risk of naming collisions. If there’s something else using the name ‘Balloon’ in our global namespace, your code is going to read ambiguously, and the javascript compiler is just going to use whichever definition of that name has higher scope priority.

Enter the Modules

This is the reason for using the methodology that AMD provides. All of your modules are explicitly scoped, and their dependency modules are all required in using something like NodeJSRequireJS, Almond, Browserify, or some other dependency management utility. Those modules are listed as dependencies, and are assigned to variable references in the current scope, for use within your particular module. In addition, some dependency management utilities will allow you to specify alternative modules via a configuration setting, to be pulled in whenever certain names are listed as dependencies. This means that you can easily swap in interface-compatible modules for use during development, staging, or production, depending on what (if any) additional functionality you may want. We’ll go into that in more detail in a minute.

Here’s an example of what our code might look like if we were to break the Balloon definition out into its own module, and work with it in a separate file for the current page:

// Balloon module: constructor and prototype definition
define(function () {
    function Balloon(color) {
        this.color = color;
        this.fillPercent = 0;
        this.popped = false;
    }

    Balloon.prototype = {
        inflate: function() {
            if (!this.popped) {
                this.fillPercent += 5;
                console.log('inflating ' + this.color + ' balloon...');

                if (this.fillPercent > 100) {
                    this.pop();
                }
            }
        },

        pop: function() {
            this.fillPercent = 0;
            this.popped = true;
            console.log(this.color + ' balloon just popped!');
        }
    };

    return Balloon;
});

We can store this file in a subdirectory, ‘classes’, and use Balloon.js for a filename. RequireJS allows for both named and unnamed modules, and if you don’t supply your module with an explicit name, it will use the filename as the name, minus the extension. We use this closure to define our Balloon constructor and prototype, and at the end of it, we return ‘Balloon’ as what our module’s definition is actually going to be.

Now, to use this module in another file, we set up a similar define() function wrapper, but pass an array as the first parameter, which holds a list of any modules this code is dependent on. This is how we get around naming collisions, by explicitly defining what our dependencies are, and using dependency injection to create local variable instances of those module definitions. Once all of those modules have finished being loaded, it will execute our callback function, passed as the second parameter, which will now reference the local variable instance of the Balloon definition, as defined by our imported Balloon module.

// Main code for current page
define([
    'classes/Balloon'
], function (
    Balloon
) {
    var balloons = [];
        i, j, k, l;

    balloons.push(new Balloon('red'));
    balloons.push(new Balloon('green'));
    balloons.push(new Balloon('blue'));

    for (i = 0, j = balloons.length; i < j; i++) {
        l = Math.floor(Math.random() * 30) + 1;
        for (k = 0; k < l; k++) {
            balloons[i].inflate();
        }
    }
});

This is now starting to get to be some beautiful code, with each piece of it maintaining its own separation of concerns for ease of consolidating logic, and future maintenance, but we still have our console.log statements hard-coded into the Balloon.prototype functions, and what happens when we’re testing this code in IE7? We get exceptions for no console object being present. What happens if we also want to be able to use this code within NodeJS, and want to use something like Winston to handle our logging output? Wouldn’t it be nice if we could have a single logging function that was aware of the environment in which it was being executed, and behaved accordingly?

First, let’s start off by abstracting our console.log statement out into its own separate module:

// Log function definition
define(function () {
    var log = window.alert;
    if (typeof console !== undefined && typeof console.log === 'function') {
        log = console.log;
    }

    return log;
});

We can name store this in a ‘utilities’ directory, and call it log.js, which means RequireJS will just call this module ‘utilities/log’. You’ll notice that we default our log variable to the window.alert function. This is for legacy IE support, where a valid console object isn’t present. We then check for the presence of a console object with a log() function, and overwrite the log variable if that function exists. We return whichever version we’re left with, to whatever code is incorporating this module. In the case of our demo program, that’s the Balloon module itself, since our main code doesn’t directly use any logging itself. We can modify our Balloon module slightly to account for this new dependency.

// Balloon module: constructor and prototype definition
define([
    'utilities/log'
], function (
    log
) {
    function Balloon(color) {
        this.color = color;
        this.fillPercent = 0;
        this.popped = false;
    }

    Balloon.prototype = {
        inflate: function() {
            if (!this.popped) {
                this.fillPercent += 5;
                log('inflating ' + this.color + ' balloon...');

                if (this.fillPercent > 100) {
                    this.pop();
                }
            }
        },

        pop: function() {
            this.fillPercent = 0;
            this.popped = true;
            log(this.color + ' balloon just popped!');
        }
    };

    return Balloon;
});

Now, whenever the Balloon class is imported as a dependency into any other code, it, in turn, will import the log utility to use internally. The beauty of some of the dependency management utilities out there is that they can also crawl all of your dependent code, find their dependencies, and those modules’ dependencies, etc, and bundle everything up into one nice optimized, single-file bundle for deployment, which is especially useful in single-page applications, which are gaining in popularity, thanks in part to AMD itself. So, we still have the problem of this module not using Winston in a NodeJS environment, so how can we deal with that? The answer comes in the form of a fairly new philosophy, that of writing isomorphic javascript, or in other words, javascript modules that can be run on either the client or the server via NodeJS. The beauty of this pattern of development is that you only need to write the business logic of your module once, and differ the exposure method based on environment, and you only have to maintain it in one location for either the client or the server.

NodeJS Module Definitions

NodeJS uses a slightly different module pattern than that of RequireJS. Its own is based on a CommonJS structure that predates RequireJS. The two are nearly identical, except that NodeJS expects the module’s return to be explicitly assigned to a ‘module.exports’ property that exists in a NodeJS environment. Armed with this knowledge, we can restructure the way our Balloon and log modules are written, to allow for them to be used either on the client or the server with equal ease. We’ll start by adding some type checking to determine the environment in which we’re loading:

(function () {
    if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
        var log = require('./utilities/log');
        module.exports = Balloon;
    } else {
        define([
            'utilities/log'
        ], function (
            log
        ) {
            return Balloon;
        });
    }

    function Balloon(color) {
        this.color = color;
        this.fillPercent = 0;
        this.popped = false;
    }

    Balloon.prototype = {
        inflate: function() {
            if (!this.popped) {
                this.fillPercent += 5;
                log('inflating ' + this.color + ' balloon...');

                if (this.fillPercent > 100) {
                    this.pop();
                }
            }
        },

        pop: function() {
            this.fillPercent = 0;
            this.popped = true;
            log(this.color + ' balloon just popped!');
        }
    };
}());

And in our log utility itself:

(function () {
    if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
        var winston = require('winston');
        module.exports = winston.info;
    } else {
        define(function () {
            var log = window.alert;
            if (typeof console !== undefined && typeof console.log === 'function') {
                log = console.log;
            }

            return log;
        });
    }
});

Now we’re talking! These modules will now both determine whether they’re running in a NodeJS environment by checking for the presence of a module.exports object, and if it exists, they’ll use that, otherwise, they’ll use the define() function to set up a client-side module definition. In the case of us being in a NodeJS environment, it will take the extra step of loading the Winston module for logging, and return its info() function as the definition for our ‘log’ utility module. As far as the consuming code is concerned, there’s no change whatsoever in how it’s used. We could also have set up a wrapper function that acted as a decorator for the Winston.log() function, or some custom logger we created through the Winston library.

Conclusion

AMD development patterns lend themselves well to dependency injection, which helps us rewrite the rules of our program at compile- or run-time. So long as the interface on a given module is the same as the interface that’s expected by its consumers, you can swap the actual module definitions out at will. This can allow us write isomorphic javascript that can be run on either the client or the server, bundle in logging functionality for development environments, polyfill missing functionality for legacy browsers, or any number of additional things we might want to do in an abstracted fashion.

Once a module has been loaded into memory, further calls to import it will simply return instantly. This is helpful when you have multiple modules you’re loading, which may share dependencies. Once the first module has loaded its dependencies, the second module with shared dependencies will load fairly instantaneously, since its own dependencies are now in memory as well.

Writing isomorphic javascript allows us to consolidate all of our business logic in one place, whether it’s being used on the client- or server-side. This gives you a single point of maintenance, and prevents features from diverging over time, which can happen when you’re maintaining two separate pieces of code for use in either the client- or the server-side.

I hope this post helped teach you all about AMD if you weren’t already familiar with it, and if you were, I hope you still managed to learn something about how to structure your code in an AMD-style paradigm. If you have any questions about something I missed, or comments in general, please feel free to post them in the section below.

Leave a Reply

Your email address will not be published. Required fields are marked *