Making my monster-class-decoupling pattern mixin-proof

Some time ago, I wrote an article about a pattern I came up with to structure monster files in JS. It's a great way to decouple the interface and the implementation in Node.JS modules. However, it has one weakness for its purpose. Once I try to compose multiple class-like types with mixins (mics in my case), as opposed to using the prototype inheritance chain, the whole re-defining thing of methods fails. That's because I suddenly don't have the constructor any more, but rather a function which returns the constructor:

const Foo = superclass => class Foo extends superclass {
  constructor() {
    super();
    this.baz = 'Hello';
  }

  bar() { throw new Error('Not implemented!'); }
};

// Will not work anymore!
// Foo.prototype.bar = function() {
//   console.log(this.baz + ' World!');
// };

So, how can I still decouple the interface and the different implementations, but at the same time have all the advantages of composable mixins?

Until now, I always started implementing the interface. It was the first thing and then I wrote the implementations, which overwrote the dummy-code on the interface. In order to use mixins, though, I have turn the whole order around, which is a little counter-intuitive at first, but makes total sense.

My initial new idea

Let's have regular functions, which are called in the scope of the object. In one file:

const bar = function bar() {
  console.log(this.baz + ' World!');
}

const Foo = superclass => class Foo extends superclass {
  constructor() {
    super();
    this.baz = 'Hello';
  }

  bar() { bar.call(this); }
};

In order to get rid of many manual require statements later on (and make use of a dynamic module loader, like node-mod-load), it is useful to create one object, on which all functions are bound and then centrally called from the interface.

But isn't that unperformant?

Yes, there is an additional call for any method I want to execute. That's no good. Luckily, there is one way out: instead of taking time for a second function call, the calculations can be shifted to the instantiation-time, which often is a one-time cost, hence not that important, especially for long-lived objects, which need speed while they live, not while they are created. The trick: use dummy implementations in the interface (just like the old approach) and assign the central function collection to the local scope. It still takes a bit longer to construct such a superclass, but I gain a pattern-advantage. While I don't think, the additional time consumption is human-perceptible, the architectural difference between decoupled code and mega-files/code repetition can be huge!

So, here is a full example

// interface/Foo.h.js
'use strict';

const mix = require('mics').mix;
const nml = require('node-mod-load')('Foo');

module.exports = mix(superclass => class Foo extends superclass {
  constructor() {
    super();
    nml.libs.collection._init.call(this);
  }

  bar() { throw new Error('Not implemented!'); }
});
// src/Foo._init.c.js
'use strict';

const nml = require('node-mod-load')('Foo');

nml.libs.collection._init = function fooConstructor() {
  this.baz = 'Hello';
  Object.assign(this, nml.libs.collection);
};
// src/Foo.bar.c.js
'use strict';

const nml = require('node-mod-load')('Foo');

nml.libs.collection.bar = function fooBar() {
  console.log(this.baz + ' World!');
};
// index.js
'use strict';

const path = require('path');

const nml = require('node-mod-load')('Foo');

nml.addMeta('collection', {});
nml.addDir(`${__dirname}${path.sep}interface`, true);
nml.addDir(`${__dirname}${path.sep}src`, true);

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

const Foo = require('.');

const foo = new Foo();

foo.bar(); // prints "Hello World"

// BUT: mixins are very possible, now!
const mix = require('mics').mix;

const Mixed = mix(
  Foo,
  mix(superclass => class extends superclass {
    bax() { console.log('I am bax!'); }
  }),
  superclass => class Mixed extends superclass {}
);

const mixed = new Mixed();
mixed.bar(); // prints "Hello World"
mixed.bax(); // prints "I am bax!"

Wow, that's a lot of boilerplate. Really, do not use this for small modules! Only use it for big modules you want to mix later on, or you might have a boring time writing all that setup code.

However, if that pattern matches your needs: check out that test file above. The code is so easy to use and compose. Adding new methods is as easy as creating a new file for them and adding the dummy-code and documentation to the interface. Adding mixins is just passing them to the mix-method. Everything is auto-wired by node-mod-load and the Object.assign() statement! I have not seen a better pattern, yet, to do exactly that :)

Marco Alka

Software Engineer, mainly working as FullStack and DevOps developer at Robert Bosch GmbH. As a hobby, I also do Game Dev, Embedded IoT tinkering & I mentor @ https://mentorcruise.com/mentor/MarcoAlka

Write your comment…

Be the first one to comment