Readable Class Definitions in Node.js

May 2nd, 2011 Permalink

I don't know about you, but I find long, brace-strewn Javascript constructors hard on the eyes - you've lost me long before adding a fifth multi-indent method to the return scope in your uberconstructor. My eye just skates off the page. If I add five multi-indent methods to the return scope of a constructor, then I'm certainly losing the instance of me who will come back in a few months to fix the bug I'm unwittingly introducing somewhere in all that nesting.

One of the pleasant things about server-side Javascript, and Node.js in particular, is that developers tend to revert to the habit of defining one class per file and spreading out their code across many files in a familiar and logical fashion. The Node.js pattern of cached requires and module.exports means that you can use these framework aspects almost as natural breaks in your code. It's a very different world from the client-side Javascript impetus to push as much as possible into a single file.

In any case, the following example shows how I divide up and lay out class definitions in Node.js for my own readability; the class is from my current slowly-progressing side project, thywill.js. Of particular note: I don't put method definitions inside the constructor, I declare the need for subclass methods by creating definitions that throw exceptions when invoked, and I assign Class.prototype to a convenience variable. You can only type "Class.prototype" so many times before being driven mad.

var util = require("util");
var Component = require("../component");

//-----------------------------------------------------------
// Class Definition
//-----------------------------------------------------------

/*
 * The superclass for interfaces between thywill and applications built on
 * thywill.js. These interfaces manage the communication between applications
 * and thywill.js.
 */
function ApplicationInterface() {
  ApplicationInterface.super_.call(this);
  this.componentType = "applicationInterface";
  this.clientInterface = null;
};
util.inherits(ApplicationInterface, Component);
var p = ApplicationInterface.prototype;

//-----------------------------------------------------------
// Initialization
//-----------------------------------------------------------

p.setClientInterface = function(clientInterface) {
  this.clientInterface = clientInterface;
};

//-----------------------------------------------------------
// Methods
//-----------------------------------------------------------

p.send = function(client, message) {
  this.clientInterface.send(client, message);
};

//-----------------------------------------------------------
// Methods to be implemented by subclasses.
//-----------------------------------------------------------

p.receive = function(client, message) {
  throw new Error("Not implemented.");
};

p.connection = function(client) {
  throw new Error("Not implemented.");
};

p.disconnection = function(client) {
  throw new Error("Not implemented.");
};

//-----------------------------------------------------------
// Exports - Class Constructor
//-----------------------------------------------------------

module.exports = ApplicationInterface;

What if I wanted the ApplicationInterface class to be instantiated as a singleton instance? Then I would set module.exports to be a newly instantiated ApplicationInterface object. The system of require() and module.exports in Node.js is an excellent methodology for maintaining singletons, in fact, since the various exports values are cached - a require() call to the same file in two different files will return the same object. So for a singleton ApplicationInterface, the end of the file would look like this:

//-----------------------------------------------------------
// Exports - Singleton Instance
//-----------------------------------------------------------

module.exports = new ApplicationInterface();

I hope you'll agree that this is very much more readable than some of what you'll see while browsing Github, especially when methods become long and nested with callbacks.