Test friendly mixins
Published
I've recently been attempting to code a clone of the classic game asteroids using canvas in the browser. Since this is me, I've been distracted by all sorts of programming detours.
This post is roughly the process I went through for one such detour.
I began planning the game by sketching out the objects it would contain. This lead to a relatively (for the subject matter) deep class hierarchy including an abstract entity as a base class, a ship class, an asteroid class, a bullet class, and so on. It quickly became obvious that it was getting convoluted, with some repetition to avoid artificial seeming relationships between these classes.
To avoid this sort of convolution, I could eschew classes in favour of plain objects and mixins, where mixins embody chunks of useful behaviour which are copied onto host objects. For example, most objects in the game can move, so I'd write a mixin function to copy a move method onto host objects.
I'm not a purist though. There's still value to a base class to encode all
essential properties of all objects in the game. Extending the base class for
other objects in the game makes sense for those behaviours and properties unique
to the child class. For example, the ship can be controlled by the user, so such
control would be defined as part of the Ship
child class. Other behaviours
will not be unique to child classes, so these can go into mixins.
There are various approaches to mixins in the wild, but I decided to roll my own. I wrote a function which builds and returns another function. The returned function applies a mixin. It looked like this:
function createMixin(descriptors) {
return function mix(obj) {
Object.defineProperties(obj, descriptors);
};
}
The argument descriptors
is an object with property descriptors. I use
these rather than simple object fields since it allows me to create mixins which
are extremely configurable.
Using this I can program the bulk of the behaviour of objects in the game. For
example, since all objects can be assumed to have position and velocity, one
such mixin could add a move
method (which I suggested earlier in this post):
const movable = createMixin({
move: {
value(dt) {
this.position.x += this.velocity.x * dt;
this.position.y += this.velocity.y * dt;
},
configurable: true, // These three properties
enumerable: false, // are like how class
writable: true // methods are set.
}
});
Consider the ship at the centre of the game. We could code it like this:
const ship = {
position: { x: 50, y: 50 },
velocity: { x: 1, y: 2 }
};
movable(ship); // Apply the mixin.
ship.move(2); // Now at x: 51, y: 52
I want the ship to be a class extending a base class though:
// Base class for all game objects.
class Entity {
constructor({ position, velocity }) {
this.position = { x: position.x, y: position.y };
this.velocity = { x: velocity.x, y: velocity.y };
}
}
class Ship extends Entity {
// Ship specific behaviour in here.
}
// Give all Ship instances access to move.
movable(Ship.prototype);
const ship = new Ship({
position: { x: 50, y: 50},
velocity: { x: 1, y: 2 }
});
movable(ship); // Apply the mixin.
ship.move(2); // Now at x: 51, y: 52
All sorts of objects and classes may be composed with mixins.
I'm a professional JavaScript programmer, which means that each time I write something there's a little voice in my head asking me how hard it'll be to write tests for it. Applying the mixin to a simple object makes writing tests for the mixin in isolation possible. In mocha:
describe('movable', () => {
it('appends a single method "move"', () => {
const entity = {
position: { x: 0, y: 0 },
velocity: { x: 0, y: 0 }
};
movable(entity);
assert.equal(typeof entity.move, 'function');
});
it('updates the position given a dt', () => {
const entity = {
position: { x: 5, y: 6 },
velocity: { x: 1, y: 2 }
};
movable(entity);
entity.move(3);
assert.deepEqual(entity.position, { x: 8, y: 12 });
});
});
I'd be more exhaustive in real world tests, but I hope you get the gist.
What about objects which have mixins applied to them though? How can we test them? Naïvely you could write the above tests for every class that uses the mixin. That's repetitive though. It would be nicer to have some mechanism to ask a mixin if an object has had the mixin applied to it. Then test for a class can use a single test for each mixin to check that the mixin has been applied, and all the repetition can be avoided.
Revisiting the mixin creating function, the first go at such a mechanism looks like:
function createMixin(descriptors) {
// References to objects the mixin has been
// applied to.
const mixed = new WeakSet();
function mix(obj) {
Object.defineProperties(obj, descriptors);
mixed.add(obj);
};
mix.isMixed = function (obj) {
return mixed.has(obj);
};
return mix;
}
I've used a WeakSet
here. A WeakSet
contains weak references to
objects. These are references which the garbage collector ignores. When no
strong references to an object in a WeakSet
remain, the garbage collector can
clear the object. If I used a regular Set
or an array here, the references
contained would be strong, and references could never be cleaned up
automatically by the garbage collector, resulting in a memory leak. This would
be a problem in particular for asteroids, as each time one is destroyed the
object itself would be held in the Set
or array be a strong reference, leading
it to grow as more and more asteroids are spawned.
Now we can ask the mixin if an object has had the mixin applied to it:
movable.isMixed(entity); // false
movable(entity);
movable.isMixed(entity); // true
What about classes though? This won't work because the mixin is applied to the prototype and no reference to an instance will be stored by the mixin. We can fix this by climbing up the prototype chain and checking each prototype object:
function createMixin(descriptors) {
// References to objects the mixin has been
// applied to.
const mixed = new WeakSet();
function mix(obj) {
Object.defineProperties(obj, descriptors);
mixed.add(obj);
};
mix.isMixed = function (obj) {
// Walk up the prototype chain and check
// if each step has had the mixin applied
// to it.
for (let o = obj; o; o = Object.getPrototypeOf(o)) {
if (mixed.has(o)) {
return true;
}
}
return false;
};
return mix;
}
This works perfectly well now, and any child classes can inherit the method as
expected and the isMixed
check will still work. It is now possible to avoid
duplication of tests. For example, part of a test suite for the ship class might
look like:
it('is movable', () => {
const ship = new Ship({
position: { x: 0, y: 0 },
velocity: { x: 0, y: 0 }
});
assert.ok(movable.isMixed(ship));
});
One final adjustment can be made. It's a shame that we can use instanceof
for
objects constructed by a class, but not objects which have had the mixin applied
to them. This can be achieved by using Symbol.hasInstance
. A first
attempt:
function createMixin(descriptors) {
const mixed = new WeakSet();
function mix(obj) {
Object.defineProperties(obj, descriptors);
mixed.add(obj);
};
mix[Symbol.hasInstance] = function (obj) {
for (let o = obj; o; o = Object.getPrototypeOf(o)) {
if (mixed.has(o)) {
return true;
}
}
};
return mix;
}
Unfortunately this doesn't work because Function.prototype[Symbol.hasInstance]
is not writable. When appending a property to something, the field you're
appending it as is checked for writability, and that check propagates up the
prototype chain. Since mix
is a function, it has a non-writable
Symbol.hasInstance
field in its prototype chain, and we need to work around
that.
We can once again use Object.defineProperty
to do it:
function createMixin(descriptors) {
const mixed = new WeakSet();
function mix(obj) {
Object.defineProperties(obj, descriptors);
mixed.add(obj);
};
Object.defineProperty(mix, Symbol.hasInstance, {
value(obj) {
for (let o = obj; o; o = Object.getPrototypeOf(o)) {
if (mixed.has(o)) {
return true;
}
}
},
configurable: false,
enumerable: false,
writable: false
});
return mix;
}
Finally we can do this:
class Ship extends Entity { /* ... */ }
movable(Ship.prototype);
const ship = new Ship({ /* ... */ });
ship instanceof Entity; // true
ship instanceof Ship; // true
ship instanceof movable; // true
So the test suite for a ship can include tests like this:
it('is movable', () => {
const ship = new Ship({
position: { x: 0, y: 0 },
velocity: { x: 0, y: 0 }
});
assert.ok(ship instanceof movable);
});
So there you have it! All the test friendliness of classes with the composability of mixins.
I wrapped this code up in a module called mixomatic, which I intend to use heavily in my gaming codebases.