State
State
is an implementation of the State
pattern in JavaScript. Simply
put, the State pattern is a way of making the behaviour of an object (i.e.
what its methods do) dependent on its state – see the linked Wikipedia article
for a good example.
// In the browser JS.require('JS.State', function(State) { ... }); // In CommonJS var State = require('jsclass/src/state').State;
State
does not implement a finite state machine. That would require
enforcing rules about which states can transition to which other states, which
events trigger transitions and what occurs during said transitions. The State
pattern simply says that you can say an object is in a certain state, and some
of its behaviour changes accordingly. The responsibility of changing an
object’s state is left entirely up to the developer.
Using the State
pattern
To borrow from the Wikipedia article on the subject, let’s imagine you’re building a drawing application. I’m just going to get the methods to return strings to indicate what’s going on. First, let’s define some state objects. Each one represents the behaviour of a set of methods in a given state, and each state should implement the same methods.
var Tools = { Pen: { mouseDown: function() { return 'Starting to draw'; }, mouseUp: function() { return 'Finished drawing'; } }, Selection: { mouseDown: function() { return 'Making selection'; }, mouseUp: function() { return 'Completing selection'; } } };
Now let’s define a class that uses the states. It must not define mouseUp()
or mouseDown()
itself – this is left up to State
. It should define an
initial state for itself when initialized.
var DrawingController = new Class({ include: State, initialize: function() { this.setState(Tools.Pen); } });
State
adds two methods to the class: setState()
and inState()
. Calling
setState()
makes sure that the instance has all the methods defined in the
given state, and sets the state of the object. inState()
takes one or more
state objects, and returns true
iff the object is in any one of them.
var d = new DrawingController(); d.inState(Tools.Pen) // -> true d.inState(Tools.Selection) // -> false d.inState(Tools.Selection, Tools.Pen) // -> true
As setState()
has added the required methods for us, we can call
state-dependent methods on the object, change its state, and see what happens.
d.mouseDown(); // -> "Starting to draw" d.mouseUp(); // -> "Finished drawing" d.setState(Tools.Selection); d.mouseDown(); // -> "Making selection"
Remember, State
does not enforce state change rules so it’s up to you to
make sure your code makes sense. It does however make it easy to manage the
behaviour of a complex object without loads of if
and switch
statements,
and to add new behaviours without modifying an object’s code directly.
State definition shortcuts
include
-ing State
in a class allows you to use a shorthand in your class
definition for adding all the class’ states. Including states in the class
itself also allows you to refer to them by name rather than as objects, so we
could rewrite the above as:
var DrawingController = new Class({ include: State, initialize: function() { this.setState('Pen'); } }); DrawingController.states({ Pen: { mouseDown: function() { return 'Starting to draw'; }, mouseUp: function() { return 'Finished drawing'; } }, Selection: { mouseDown: function() { return 'Making selection'; }, mouseUp: function() { return 'Completing selection'; } } }); var d = new DrawingController(); d.inState('Pen') // -> true d.inState('Selection') // -> false d.inState('Selection', 'Pen') // -> true d.mouseDown(); // -> "Starting to draw" d.mouseUp(); // -> "Finished drawing" d.setState('Selection'); d.mouseDown(); // -> "Making selection"
Behind the scenes, State
makes sure that all the states implement the same
methods. If one state has a missing method, a method is added to it that
simply returns the object. For example:
var Twiddle = new Class({ include: State, initialize: function() { this.name = 'Twiddle'; this.setState('Incomplete'); } }); Twiddle.states({ Complete: { getName: function() { return this.name; } }, Incomplete: { } }); var twid = new Twiddle(); twid.getName() === twid // -> true twid.setState('Complete'); twid.getName() // -> "Twiddle"
Notice that inside Complete.getName
, the this
keyword refers to the object
calling the method, just as for regular ‘stateless’ methods.
State inheritance
State
also supports inheritance using the following set of rules: Given a
class Controller
with states Pen
and Selection
, and a subclass
ChildController
,
ChildController
must have at least all the statesPen
andSelection
, and may have some more of its own.- All the states in any class must implement the same methods.
- If a state method (e.g.
Pen.mouseDown()
) exists in bothController
andChildController
, the method inChildController
can usethis.callSuper()
to callPen.mouseDown()
in the parent class.
These rules are enforced by State
so that when you write a subclass, you
only need to specify the ways in which it differs from its parent. Let’s take
an example implementation of ChildController
.
var ChildController = new Class(DrawingController); ChildController.states({ Selection: { mouseDown: function() { return this.callSuper().toUpperCase(); } }, Eraser: { mouseMove: function() { return 'Removing stuff'; } } });
So now ChildController
will have three states, Pen
, Selection
and
Eraser
, all of which have three methods, mouseUp
, mouseDown
and
mouseMove
. mouseMove
does nothing in states Pen
and Selection
, and
likewise for mouseUp
and mouseDown
in the Eraser
state. The
Selection.mouseDown
method calls the super method from the
DrawingController
class during its execution. Let’s test out our new class:
c = new ChildController(); c.inState('Pen') // -> true c.mouseDown() // -> "Starting to draw" c.mouseMove() === c // -> true c.setState('Selection'); c.mouseDown() // -> "MAKING SELECTION" c.setState('Eraser'); c.mouseUp() === c // -> true c.mouseMove() // -> "Removing stuff"
Note that you are allowed to implement mouseMove
in the Selection
state if
you so wish. The thing to remember is that anything you leave unspecified will
be filled in for you using the rules given above.