Machina.jsはJavaScript/Node.jsでステートマシンを扱えるフレームワークです。最近私が書いているNode.jsのコードでステートマシンを導入したら、便利になりそうなところがあったので導入してみました。Machina.jsを日本語で取り扱っている記事が見当たらなかったので、参考になればと思います。なんと呼べばよくわかりませんが、とりあえず私はマキナと呼んでいます。
ステートマシンって?
いわゆる有限オートマトン(FSM:Finite State Machine)です。システムの有限個の状態とその関係を遷移という矢印でつないだものですね。if文をたくさん書いて汚いコードになってしまった時、UMLのステートマシン図とかに落として整理しますね。
Machina.jsを選んだ理由
npmで'fsm'などの検索ワードで検索してダウンロードが多いモノを選びました。Machina.jsのgithubリポジトリを見ると現在の時点でスター数が893でした。これは間違いなさそうです。ライセンスもMITです。
インストール
Node.jsを利用している場合はnpmで入ります。通常のJavaScriptで使う場合は、公式ページサイトから、あるいはgithubからcloneしてきて、中にあるlib/machina,min.jsを使えば良さそうです。
$ npm install machina
基本的な使い方
公式のReadmeでも良いのですが、他にも例を探していたらわかりやすい記事があったのでそれをベースに解説します。
たとえば車のギヤのステートマシンを定義したいと思います。状態はN(ニュートラル)がデフォルト、1速、R(バック)の3つのギヤを考えます。下記の例ではそれぞれの状態においてアクセルを踏んだ時(accelerate)したときに車がどのように振る舞うかがわかります。かなりシンプルですし、コメントをたくさん書いたので見てみてください。
var machina = require('machina');
var Car = machina.Fsm.extend({
initialState: 'N',
states: {
'N': {
accelerate: function() {
console.log('the gear is not set');
}
},
'R': {
accelerate: function() {
console.log('move back');
}
},
'1': {
accelerate: function() {
console.log('move forward');
}
},
},
});
var car = new Car();
car.on('transition', function(data) {
console.log('state change:', data.fromState, 'to', data.toState);
});
car.handle('accelerate');
car.transition('1');
car.handle('accelerate');
car.transition('R');
car.handle('accelerate');
上記の例はコンストラクタ版ですが、インスタンス版でも良いと思います。
var machina = require('machina');
var car = new machina.Fsm({
});
またhandleをラップするとタイポなどがあった場合に早く見つけられそうです。
var machina = require('machina');
var Car = machina.Fsm.extend({
initialState: 'N',
states: {
},
accelerate: function() {
this.handle('accelerate');
},
shift: function(newGear) {
this.transition(newGear);
},
});
var car = new Car();
car.shift('1');
car.accelerate();
car.shift('N');
car.accelerate();
よく使いそうなもの
machina.Fsm Prototypeとして公式に一覧がありますが、よく使いそうなものを上げていきます。initialState、handle、transitionは既に前例で登場済みなのでスキップします。
_onEnter/_onExit
こちらはそのステートに入ったタイミングと次のステートに切り替わるタイミングで呼ばれます。例えば先程のRのステートのところにこれを追加すると下記のような結果になります。そのステートにおけるinitialize/finalizeはこの中ですると良さそうです。
'R': {
_onEnter: function() {
console.log('R: _onEnter');
},
accelerate: function() {
console.log('move back');
},
_onExit: function() {
console.log('R: _onExit');
},
},
state change: N to R
R: _onEnter
R: _onExit
state change: R to 1
emit/on/off
emitではオリジナルのイベントを呼び出すことができます。作ったイベントはon/offでコールバックの登録、解除ができます。ここでは例で'broken'イベントを作ってみます。非常にJavaScriptらしい書き方でイベントとコールバックを登録できますね。
'R': {
accelerate: function() {
this.emit('broken', 'aiueo');
},
},
var car = new Car();
car.on('broken', function(data) {
console.log('broken!', data);
});
car.shift('R');
car.accelerate();
broken! aiueo
deferUntilTransition
これは次のTransitionまでその関数(振る舞い)をキューイングしてくれます。例を見るのが一番分かりやすい思います。例えばネットワークのステートを考えた時にrequestHttpという何かしらのサーバーにリクエストするような関数を考えてみます。ただし、これがoffline状態の時に呼ばれてしまうとクライアント側は再度requestHttpをしないといけません。そこで、このdeferUntilTransitionを呼んでおけばrequestHttpという振る舞いがmachinaにキューに積まれます。そして、deferUntilTransitionの引数にはonlineを与えているので、ステートがonlineになっただけでonlineステートのrequestHttpが実行され、安全に処理できます。
これを使いこなせれば上級者の仲間入りができそうです。ちなみにdeferUntilTransitionの引数に何も入れないと、何らかのステートに変わったら発動する、という条件に変わるようです。
var machina = require('machina');
var Network = machina.Fsm.extend({
initialState: 'offline',
states: {
'offline': {
'requestHttp': function() {
console.log('requestHttp is deferred');
this.deferUntilTransition('online');
},
},
'online': {
'requestHttp': function() {
console.log('request...');
},
},
},
});
var network = new Network();
network.on('transition', function(data) {
console.log('state change:', data.fromState, 'to', data.toState);
});
network.handle('requestHttp');
setTimeout(function() {
network.transition('online');
}, 1000);
requestHttp is deferred
state change: offline to online
request... // -> deferされてたrequestHttpが1秒後のステートがonlineに変わったところで呼ばれる
関数としてアスタリスク(*)を書いておけば、定義されていない関数が呼ばれた場合にそれで受け止めることができます。色々例を探していると前述のdeferUntilTransitionを組み合わせて、ステートが変わったタイミングで再度その関数を呼ぼうとする例が多かったですね。これでクライアントからの呼び出しをちゃんとキャッチできる訳です。
'N': {
'*': function() {
console.log('handler is not defined');
},
var car = new Car();
car.handle('hogehoge');
handler is not defined
まとめ
JavaScript/Node.jsで使えるステートマシンライブラリのMachina.jsを試してみました。非常にシンプルでカスタマイズ性のあるフレームワークになっているのが分かりました。ただ一番重要なのはステートマシンをしっかり設計し、それを実際にコードに落とすことですね。結局設計がしょぼいと純粋にif文を羅列したほうがわかりやすいときもあるので、本末転倒にならないようにしたいところです。