Andro.js

Mix behaviours into objects

What is Andro.js?

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.

Come again?

Imagine a cube. It can be touched. You want this cube to make a sound when it goes from not being touched to being touched. You write a little behaviour that emits an event when a first touch occurs. You write another little behaviour that plays a sound when it receives a first touch event. You combine these behaviours on your cube with Andro.js.

Or, to put it another way, Andro.js a library for objects that can't quite decide who they are.

Get the code

Example

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

Why is this cool?

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.

Reference

Add a behaviour to an owner object

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().

Bind a behaviour to an event

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".

Emit events from a behaviour

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.

Unbind a behaviour from an event

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?

Export from a behaviour

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;
  }
};

Remove Andro augmentation

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);

Why might this not be cool?

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.

Licence

The code is open source, under the MIT licence.