Andro.js takes mixins and applies them, each in its own namespace, to an object, and lets them talk to one another via an event emitter.
Or, to put it another way, Andro.js a library for objects that can't quite decide who they are.
$ npm install androjs
I require the andro.js
file. I define the owner object as a constructor called Cube
. It has a touch
function that, when called, uses the andro.eventer()
function to get the eventer to emit a touch
event to all behaviours attached to the cube.
var andro = require('andro').andro; function Cube() { this.touch = function(contact) { andro.eventer(this).emit("touch", contact); }; };
I define firstTouchBehaviour
. This binds to the touch
events emitted by its owner and keeps track of the number of things currently in contact. When the owner goes from being untouched to being touched, firstTouchBehaviour
emits a FirstTouch:newlyBeingTouched
event.
var firstTouchBehaviour = { touchCount: 0, setup: function(owner, eventer) { eventer.bind(this, "touch", function(contact) { if(contact === "added") { if(this.touchCount === 0) { eventer.emit("FirstTouch:newlyBeingTouched"); } this.touchCount++; } else if(contact === "removed") { this.touchCount--; } }); } };
I define soundBehaviour
. This binds to the FirstTouch:newlyBeingTouched
event. Each time this event occurs, soundBehaviour
makes a noise: "Rarrrrrwwwwwwwwwwwwwwww".
var soundBehaviour = { setup: function(owner, eventer) { eventer.bind(this, "FirstTouch:newlyBeingTouched", function() { console.log("Rarrrrrwwwwwwwwwwwwwwww"); }); } };
I now put everything together. I instantiate cube
. I augment it with firstTouchBehaviour
and soundBehaviour
. I simulate two touches upon the cube. On the first, it roars. On the second, it does not.
var cube = new Cube(); andro.augment(cube, firstTouchBehaviour); andro.augment(cube, soundBehaviour); cube.touch("added"); // rarrrww cube.touch("added"); // silence
Behaviours are completely separate from each other, so they are reusable. You could write explodeBehaviour
, combine it with firstTouchBehaviour
and have exploding cubes. Or you could write wetBehaviour
, combine it with explodey and touchey and have depth charges.
Behaviour mixins are good for writing video games. They help with the concoction of game object logic: no more nightmarish inheritance hierarchies or weird bundles of functions that invent cryptic interfaces for the objects upon which they operate.
See the end of this document for why Andro.js might not be cool.
A behaviour is a JavaScript object that has some properties and functions. It can be an empty object, if you like. Here is a behaviour that is not an empty object, but still does absolutely nothing:
var wooBehaviour = { setup: function(owner, eventer, settings) { this.owner = owner; } };
We add this behaviour to the owner object by calling augment()
and passing in ownerObject
, wooBehaviour
and an optional settings
object. This creates a behaviour object, adds it to the ownerObject.behaviours
array and writes the properties and functions of the behaviour to it.
var ownerObject = {}; andro.augment(ownerObject, wooBehaviour, { followUp: "hoo" });
wooBehaviour
includes the optional function, setup()
, that will automatically be run when we call augment()
. When it is run, setup()
receives an owner
argument that is bound to the owner of the behaviour, and the optional settings
object, if one was passed to augment()
.
We can improve the version of wooBehaviour
from the previous section so that, when it receives an event, woo
, it goes "woo":
var wooBehaviour = { setup: function(owner, eventer, settings) { this.owner = owner; eventer.bind(this, "woo", function() { console.log("Woo."); }); } };
The setup()
function now calls bind
, passing the behaviour, event name and a callback. Now, whenever the event, woo
, is emitted, our behaviour will go, "woo".
We can further improve the wooBehaviour
from the previous section. As well as going "woo", it will emit an event, hoo
.
var wooBehaviour = { setup: function(owner, eventer, settings) { this.owner = owner; this.eventer = eventer; this.followUp = settings.followUp; eventer.bind(this, "woo", this.woo); }, woo: function() { console.log("Woo."); this.eventer.emit(this.followUp, "Woo"); } };
We emitted the hoo
event that was passed in via settings
. We sent along an identifying string, Woo
, as data. That way, anyone who binds to the hoo
event will know who is doing the hooing.
Let's say we become worried that we are going a bit mental with the wooing. We can alter our behaviour so that, after wooing once, it unbinds from the woo
event, thus cutting out any future woos.
woo: function() { console.log("Woo."); this.eventer.emit(this.followUp, "Woo"); this.eventer.unbind(this, "woo"); }
See how on line four it calls unbind on the eventer, passing in the behaviour and the event to unbind from?
To gild the lily, we will make wooBehaviour
respond to enquiries about whether "woo" has been said. To do this, we add an attribute called hasWooed
and alter the woo
function to set hasWooed
to true
. We add a function, getHasWooed()
, that returns hasWooed
. Finally, we alter setup()
so that it returns an object that includes getHasWooed
. This function will get written onto the owner object so it can be used by the owner, or by other objects.
var wooBehaviour = { hasWooed: false, setup: function(owner, eventer, settings) { this.owner = owner; this.eventer = eventer; this.followUp = settings.followUp; eventer.bind(this, "woo", this.woo); return { "getHasWooed": this.getHasWooed }; }, woo: function() { console.log("Woo."); this.hasWooed = true; eventer.emit(this.followUp, "Woo"); eventer.unbind(this, "woo"); }, getHasWooed: function() { return this.hasWooed; } };
When we grow tired of all the wooing and hooing, we can restore our faithful old object to its pre-Andro state by calling tearDown()
. This will remove the andro object from the owner object and unbind all event callbacks.
andro.tearDown(ownerObject);
Andro.js contains four modes of expression: mixins, exports, settings and event emitters. The library protects you from harming your owner objects: behaviours are name-spaced and you cannot give an export the same name as an existing owner attribute. However, armed with an event emitter and mixins, you can still express yourself into a hell of a mess. Therefore, I humbly offer you some things that seem to be true more often than not.
Behaviours should have one responsbility: flash a light, make a sound, register touches.
Status behaviours are good. That is, behaviours that solely talk about what is going on. Like, "My owner is no longer being touched by anything".
Status-consuming behaviours are also good. Like, "Every time I hear that my owner has been touched, I make a sound."
Exported functions that allow the interrogation of state are OK. Like, "Here you are, owner. Here is a function that others can use to find out if anything is touching you."
Changing the state of the owner, either via an exported function or from within a behaviour: bad.
Behaviours that mediate between other behaviours are hard to understand. Like, "Every time I hear my owner has been touched, I emit an event that tells the sound behaviour to pipe up." Use these sparingly and name them well.
The code is open source, under the MIT licence.