Decoupling Node.JS Monster Files

This is something I learned the hard way. Have you ever written an application which started as a mockup, but mutated to a monster with few files which each contained a few thousand Lines of Code? Well, I have. It was ugly.

  • [CTRL] + [F] ... is what I had to use all the time just to search the functions and methods I need... like NOW
  • Setting breakpoints and then going out of current scope always took me on an adventurous trip all around the files, never knowing where I might end up next
  • The documentation in code was scattered all over the file and hard to find. If anyone asked me "what does the interface look like", I had to start searching and working a lot on an answer
  • Because the monster just grew and became more and more of a hassle to work with, my motivation went down, too. It was no more fun to do stuff

So I decided it was time for a major clean-up. I really had to reorganize the code or everyone would hate me for it; me myself included.

Finding a solution

I know that I have to decouple things. Short files are readable and allow for quickly scanning the content. Also, when it's just tiny pieces of logic, grasping what the code does is a lot more easy - or at least it feels more manageable. For coming up with a solution to my problem, I first tried to ask Google and I even asked direct questions to different communities I knew - without any good result. That's when I took a look at languages I know.

Delphi. My first language. I remember that I saw Embarcadero core libraries with ten thousands (maybe more) of lines of code. Not a good start, that's exactly what I want to fix.

C++. The language I use the second most (after JS). In C++, I have header files and code files. I can write as many code files as I need and declare the interface in one place, which makes for a good overview. PERFECT! I want to be able to put my interface into a "header" file and put the code into "code" files.

That was nearly too easy. I knew what I wanted, but I was not quite sure: will it work?

Building a prototype

So what I did was think hard. How can I separate my code. The answer has to be in the way how Node.JS stiches together all the modules it loads. I started reading the code for the require function, learned how modules are built and how they are cached. Then, I came up with the following idea: I have to build a class, which will only serve as interface. I will export it. The export will be cached. Then I will require the cache and overwrite all prototype functions and static methods. For better overview, every method should get its own file. In order to have an easy way to include all those files, I need another file; the "glue" file. The glue is pretty simple and does not need to be a stupid list of requires. I already wrote a module loader in the past, which can load all files and packages from a given directory (node-mod-load, if you are interested. I rewrote its codebase to reflect this new pattern). That makes things very easy.

So here is my first simple prototype:

// glue.js
'use strict';

module.exports = require('header.js');
require('foo.js');
// header.js
'use strict';

module.exports = class Test {

  constructor() { /* I do not want to put code here, need a solution! */ }

  /**
   * Can easily put documentation here
   */
  foo() { /*definition somewhere else */ }
};
// foo.js
'use strict';

require('./header').prototype.foo = function foo() {

  // Can implement code here, nice!
};

Conventions I developed over the past months

From the prototype above you can see that the interface with all in-line documentation is centralized, but I still had a few open issues left. I also stumbled over a few more issues. For example how do I know that I forgot to include a file, etc. That's why, in order to use this new pattern efficiently, I had to make a list of conventions I follow strictly. Here they are, for your convenience:

  • Put headers into the directory /interface and code files into the directory /src
  • Name all files (except for the glue file) according to the schema <class>[OPTIONAL: .<method>].<type>.js, for example foo.h.js, foo.bar.c.js, foo.baz.c.js
  • All methods in the interface have to throw Not Implemented: <method_name>
  • Helper functions go into the file of the method they belong to
  • All public attributes should have their initial values assigned in the interface (constructor)
  • The constructor in the interface must call the private method _init, which will implement the real constructor code

When everything is put together, the result looks like this:

// /index-test.js
'use strict';

var nml = require('node-mod-load')('test');
nml.addDir('./interface', true);
nml.addDir('./src', true);

module.exports = nml.libs['test.h'];
// /interface/test.h.js
'use strict';

module.exports = class Test {

  constructor() { this._init(); }

  /**
   * Can easily put documentation here
   */
  foo() { throw 'Not Implemented: Test::foo!'; }
};
// /src/test.foo.c.js
'use strict';

require('../interface/test.h').prototype.foo = function foo() {

  // Can implement code here, nice!
};
// /src/test._init.c.js
'use strict';

require('../interface/test.h').prototype._init = function _init() {

  // Do constructor stuff right here
};

How this pattern might develop in the future

The biggest problem I see is that ES6 modules are standardized and will be implemented in Node.JS at some point in the future. I do not have enough experience to tell if this pattern will work the same with ES6, but I am pretty sure that I will have to rework at least part of it. For now, the pattern I presented here has an initial boilerplate, but shines later on (hang in there in the beginning!). From what I have seen, the average file size is about 50-100 LoC. I was able to break down many monsters this way and at the moment I am reworking my opensource projects :)

If you know my problem (first hand) and you are at a loss (just how I was), please feel free to give this pattern a shot and then give me feedback if you like it and how I can further improve it.

> I improved it to be mixin-proof

No Comments Yet

Add a comment