Qubyte Codes

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.

Backlinks