Articles in This Series
Using a classical analogy, a class should abide by the single responsibility principle from SOLID. Likewise, a module should also have a single responsibility, but In this case the single responsibility is that a given module is a one-stop-shop for handling related tasks.
Returning to our error handling module discussion from a previous article, this module may have many distinct units of functionality (i.e., functions), but they should all be related to handling errors. In addition, the module should not contain any functionality not required for handling errors, and it should implement any and all error handling requirements for a given application.
This error handling module should be consumable by your application, or other modules as required given its cross-cutting nature. Even better, it could be loosely coupled from the rest of your application through an event-driven pattern like Pub/Sub, which we will cover in a later article.
Most of the time it boils down to your preference and what works best for your particular situation. There is no shortage of very strong opinions online about what module implementations are best, so feel free to search around a bit if you’re still unsure.
Example: POJO module pattern
In order to create a closure and ensure all variables and functions are local to the module’s scope, practitioners will often wrap the module with an IIFE (immediately-invoked function expression).
Example: Scoped module pattern
Another pattern is the module pattern itself, along with the so-called revealing module pattern. The revealing module pattern is a special case of the module pattern that implements a form of hidden private members, and thus exposes a public interface. Here is an example of the revealing module pattern.
Example: Revealing module pattern
Example: Prototype pattern
Module Formats and Specifications
In order to address these concerns, a variety of module formats and specifications have surfaced that are intended to promote the following: standardization and consistency, encapsulation, asynchronous and parallel script loading, increased application performance, improved readability and code cleanliness, reduction of HTML script tags, and dependency management. The three biggest players are AMD (Asynchronous Module Definition), CommonJS, and the ECMAScript Harmony module specification.
Let’s examine each of these options individually and at a high level. Note again that this series is intended to be an overview of available choices, architectural considerations, and best practices, but not to give strong opinions on which options are best. For additional information on these topics and options, refer to the ‘Resources’ section at the end of this article.
Asynchronous Module Definition API (AMD)
Asynchronous Module Definition, or AMD, is a specification for defining modules and their dependencies, which can be loaded asynchronously by the browser. The Require.js file and module loader by James Burke is likely the most popular framework based on the AMD specification, but is not the only AMD implementation available.
Require.js is highly configurable, customizable, very well implemented, and widely used. It uses the AMD syntax, which specifies the
require functions. Common complaints about Require.js and AMD in general are that it can be fairly complicated to configure, and the syntax is not exactly terse. Also, Require.js is more of a client-side (a.k.a. browser-based) module implementation framework by design.
Despite these complaints, there are certainly some upsides to embracing AMD and Require.js. The first is that Require.js is very good at what it does. Assuming that you have everything set up properly, you can be assured that your modules and their dependencies will be loaded asynchronously and in whatever order specified by your configuration.
The other advantage of using Require.js is that it has a built-in optimizer that can be used to combine and minify scripts (via UglifyJS or Google’s Closure Compiler), as well as inlining and optimization of CSS files via the @imports directive and comment removal.
CommonJS is another module specification that is widely used, and specifies the
require functions. It’s a module format intended to be used by both browsers and servers alike, although it’s most commonly implemented in server-side frameworks like Node.js.
Browserify is a very popular client-side CommonJS module framework. You can implement a full stack CommonJS module solution when combining Browserify on the client, and Node.js on the server for example.
Some common complaints of the CommonJS specification are reliance on server-side tools and build processes, being too server-driven in general, cross-domain issues, and lack of transport format. Advantages of CommonJS include being relatively easy to setup and configure, terse syntax and implementation, easy to read, diverse, and can be implemented across the entire stack. It is also able to handle cyclical dependencies very well.
ECMAScript 6 Harmony
The current edition of the ECMAScript standard is 5.1, although ECMAScript 6 (also known as Harmony and ES.next) is the next edition of the standard to be officially published.
ES6 modules consist of a declarative importing and exporting syntax, and programmatic loader API for module loading and dependency management. Unlike AMD and CommonJS, which implement a few very key functions (define, require, and exports), ES6 modules are keyword driven. The keywords
export are the primary players. When an exported module member has a name declared, it becomes a named export.
In addition to named exports, ES6 defines a default export, which is meant to represent the single and most important exported value per module. The value can be whatever you want (function, object, etc.), but the default export is the key exported item for a given module. Note that a module can have a default export along with named exports.
Additional features of ES6 modules include support for both synchronous and asynchronous loading, as well as a programmatic and configurable module loader API. Note that ES6 modules are not implemented yet by all major browsers and may require a transpiler to use now.
I highly recommend choosing a module pattern or specification that makes the most sense for you. Once you have settled on one, be sure to use it consistently throughout your application. Consistency is a key component to readability and maintainability!
In the next article, we will discuss patterns used to minimize coupling between modules, which can heavily contribute to an application’s scalability, reusability, reliability, and maintainability.