Sunday, June 16, 2013

Building a Module Factory in Node.js

As part of my Steam Chat Bot I was trying to figure out a good way to make a factory in Node.js that could keep a registry of a bunch of subclasses and construct them based on the name of the class. I also wanted  to be able to save the details of a set of these subclasses as JSON so that they could be stored and loaded, and I wanted to have some way of loading the subclasses dynamically, so that I could just create a new file, drop it in the right folder, and be able to create an instance of the new subclass object without restarting my app. I'm sure a good instructional blog post on how to do this exists somewhere but I couldn't find it, and since I figured out a solution I thought I'd go through it.

To keep things simple, I'll go through the creation of a factory that creates different types of ducks, which will be simple objects that just have a single "quack" function that returns a string based on the type of duck and an attribute of the class. Whenever we call a create function on the factory, it will scan a folder to load all the different types of duck, and create a new duck object of a particular type. This way, we can add or modify files in the "ducks" folder and we'll be able to get a new object with those changes without taking down our app.

So first, here is our factory object, stored at ./duckFactory.js:
var fs = require('fs');

var DuckFactory = function() {
 this.duckTypes = {};
};

// Go through all files in the ducks directory and register the 
// create function to the duckType
DuckFactory.prototype.loadModules = function() {
 var files = fs.readdirSync('./ducks/');
 this.duckTypes = {};

 for (var i=0; i < files.length; i++) {
  var moduleName = './ducks/' + files[i];

  // Clear the module cache so module updates can be loaded
  delete require.cache[require.resolve(moduleName)];
  var module = require(moduleName);

  if (module.duckType && module.create) {
   this.duckTypes[module.duckType] = module.create;
  }
 }
}

// Create a duck with the specified type
DuckFactory.prototype.createDuck = function(type, name, details, refresh) {
 if (refresh) {
  this.loadModules();
 }

 if (type in this.duckTypes) {
  return this.duckTypes[type](name, details);
 }
 return null;
}

exports.DuckFactory = DuckFactory;

Whenever a duck is requested, we scan through a folder and load in each file as a module. We check to make sure that the module has a duckType string and a create function in its exports, and then tie the string to the function. Since we are clearing the module cache before loading the module using "delete require.cache[require.resolve(moduleName)];", we will always get the latest version of the module as well as any new module files that have been added to the folder. Deleting the cache isn't usually recommended since it can cause dependency cycles, but I trust myself to not do anything silly like that. Note that this factory leaves open the option to refresh the modules on creation, or externally by the client as needed.

Now that we know that a module just needs a duckType string and a create function, we can write a simple base duck class (./ducks/baseDuck.js):
var BaseDuck = function(type, name, details) {
 this.type = type;
 this.name = name;
 this.details = details || {};
};

exports.BaseDuck = BaseDuck;
var type = "BaseDuck";
exports.duckType = type;
exports.create = function(name, details) {
 return new BaseDuck(type, name, details);
};

BaseDuck.prototype.quack = function() {
 throw "abstract function";
}

There isn't much to say about this, it's a pretty standard base class, except for the fact that most constructor parameters are specified as a map instead of separate variables so that it can be a standard signature across classes with different options. I could have left this file out of the ducks folder, so that it was not possible to create a BaseDuck object, but I thought it was a bit neater to leave it there. It also leaves the option open to create a BaseDuck and override the quack function on that object, in case that is ever useful.

Now lets create a concrete subclass, MallardDuck (./ducks/mallardDuck.js):
var util = require('util');
var BaseDuck = require('./baseDuck.js').BaseDuck;

var MallardDuck = function() {
 MallardDuck.super_.apply(this, arguments);
};

util.inherits(MallardDuck, BaseDuck);

var type = "MallardDuck";
exports.duckType = type;
exports.create = function(name, details) {
 return new MallardDuck(type, name, details);
};

MallardDuck.prototype.quack = function() {
 return this.details.attitude + " quack!";
}

The most important thing to note about this file is that it uses the inherits function from the utils module. This behaves similar to inheritance in object-oriented languages, but is implemented in a different way. It simply copies all the base classes prototype functions into the subclass, as well as copying the base classes constructor as a new function called _super. Although in our case we don't have any useful prototype functions in our base class, in real life we could put any shared duck functionality in the base class and automatically have access to it in our subclass. We do use the _super function though, passing it the arguments object so that we could conceivably change the signature of the base classes constructor without needing to change all the subclasses.

Let's also create a RubberDuck class for some variety (./ducks/rubberDuck.js):
var util = require('util');
var BaseDuck = require('./baseDuck.js').BaseDuck;

var RubberDuck = function() {
 RubberDuck.super_.apply(this, arguments);
};

util.inherits(RubberDuck, BaseDuck);

var type = "RubberDuck";
exports.duckType = type;
exports.create = function(name, details) {
 return new RubberDuck(type, name, details);
};

RubberDuck.prototype.quack = function() {
 return "squeak in the " + this.details.speciality;
}

Now we'll make a fairly simple client that can demonstrate the use of the factory (./client.js):
var sys = require("sys");
var DuckFactory  = require('./duckFactory.js').DuckFactory;

var duckFactory = new DuckFactory();
var stdin = process.openStdin();
var ducks = {};

stdin.addListener("data", function(input) {
 input = input.toString().trim();
 var words = input.split(" ");
 if (words.length > 1) {
  var command = words[0];
  var params = input.replace(command, "").trim();

  if (command == 'create') {
   var paramMap = JSON.parse(params);
   createDuck(paramMap.type, paramMap.name, paramMap.details);
  }
  else if (command == 'quack') {
   quackDuck(params);
  }
 }
});

function createDuck(type, name, details) {
 var duck = duckFactory.createDuck(type, name, details, true);
 if (duck) {
  console.log("> Created " + duck.name + " of type " + duck.type);
  ducks[duck.name] = duck;
 }
 else {
  console.log("> No class found with name " + type);
 }
}

function quackDuck(name) {
 var duck = ducks[name];
 if (duck) {
  console.log("> " + duck.name + " the " + duck.type + 
   " says '" + duck.quack() + "'");
 }
 else {
  console.log("> No duck found with name " + name);
 }
}

We can use this client to create a MallardDuck and a RubberDuck, and we'll also try to create a RoboDuck which we haven't written yet.
node client
create { "name": "Adam", "type": "MallardDuck", "details": { "color": "brown", "attitude": "feisty" } }
> Created Adam of type MallardDuck
create { "name": "Bob", "type": "RubberDuck", "details": { "size": "small", "speciality": "bath" } }
> Created Bob of type RubberDuck
create { "name": "Carl", "type": "RoboDuck", "details": { "primeDirective": "DESTROY" } }
> No class found with name RoboDuck
quack Adam
> Adam the MallardDuck says 'feisty quack!'
quack Bob
> Bob the RubberDuck says 'squeak in the bath'
quack Carl
> No duck found with name Carl

Without quitting the app, we can make a change to MallardDuck (just removing the exclamation mark from the quack function) and add a new RoboDuck class file to the ducks folder (./ducks/roboDuck.js):
var util = require('util');
var BaseDuck = require('./baseDuck.js').BaseDuck;

var RoboDuck = function() {
 RoboDuck.super_.apply(this, arguments);
};

var type = "RoboDuck";
exports.duckType = type;
exports.create = function(name, details) {
 return new RoboDuck(type, name, details);
};

util.inherits(RoboDuck, BaseDuck);

RoboDuck.prototype.quack = function() {
 return "I WILL " + this.details.primeDirective + " YOU!!!";
}

Now the magic happens, we can make a RoboDuck and an updated MallardDuck and see the results in our still-running client.
create { "name": "Carl", "type": "RoboDuck", "details": { "primeDirective": "DESTROY" } }
> Created Carl of type RoboDuck
create { "name": "Doug", "type": "MallardDuck", "details": { "color": "black", "attitude": "emo" } }
> Created Doug of type MallardDuck
quack Adam
> Adam the MallardDuck says 'feisty quack!'
quack Bob
> Bob the RubberDuck says 'squeak in the bath'
quack Carl
> Carl the RoboDuck says 'I WILL DESTROY YOU!!!'
quack Doug
> Doug the MallardDuck says 'emo quack'

There is still a bit of scaffolding code required in the subclasses, but I think this is a pretty neat solution. I've put the code up at github, and if anyone knows of a better way of implementing this type of factory, I'd love to hear it.

1 comment:

Note: Only a member of this blog may post a comment.