TesselでState パターン

f:id:two-hats:20141230061125j:plain
※2014/1/4…以前のコードはオーバーライドでprototypeを全て書き換えてしまう書き方になっていたので、部分的にオーバーライドできるような書き方に修正しました。


前回の記事でも書きましたがJavascriptでプログラムできる「Tessel」でちょこちょこと遊んでいます。

TesselはWifi標準搭載でモバイルバッテリーでも起動するので、スタンドアロンなガジェットをサクッと作るには調度良いマイコンかと思います。
実際に何かを作ることになったらセンサやボタンなどを複数取り付けることになり、状態変化の制御に四苦八苦しそうなのでステートパターンが使えないか試してみました。

結論:継承にnode.jsのinheritsを使ったら動いた。


ステートパターンの書き方

ステートパターンは何者や?というのはググって頂いた方が良記事が沢山みつかると思うので割愛します。ここではTesselで動いたステートパターンの書き方を紹介します。

まずは状態を代入する変数stateと、動作を纏めた状態の抽象クラスを作成します。

var state = null;// 状態の代入先

function AbstractState() {};
AbstractState.prototype = {
    //状態によって変化する動作を纏めて定義する。
    //例:ボタンを押した/離したの制御
    onPress:function(){
      console.log('onPress');
    },
    onRelease:function(){
      console.log('onRelease');
    },
    toString: function() {
      return "AbstractState";
  }
};

この状態の抽象クラスを継承して各状態を作成します。

jQueryやBackboneなどで継承を行う歳にはextendメソッドを使うことが多いと思いますが、Tesselにそのようなボリューミーなライブラリ使うのも気が引けるし、更にLua VM(TesselはJSやnodeをLuaに変換して動作している!!)の1つのスコープに変数は200までという仕様のためjQueryもそのままでは使えないようです。

node.jsがサポートしているのでこちらを利用できないか調べてみると、utilライブラリにinheritsという継承のメソッドがあることが分かりました。extendの代わりにこちらを利用します。
ここではA状態とB状態の二つを定義することとします。

//A状態定義
function AState(name){
  AbstractState.call(this, name);
}
util.inherits(AState, AbstractState);//継承
AState.prototype.onPress = function(){
  //A状態でのボタン押下の挙動を書く
};
AState.prototype.onRelease = function(){
  //A状態でのボタン離したときの挙動を書く
  //状態遷移するにはstate変数に遷移先の新しい実態を代入する。
  if(...){
    state = new BSate();
  }
};
AState.prototype.toString = function(){
  return "AState";
};


//B状態定義
function BState(name){
  AbstractState.call(this, name);
}
util.inherits(BState, AbstractState);
BState.prototype.onPress = function(){
  //B状態でのボタン押下の挙動を書く
};
BState.prototype.onRelease = function(){
  //B状態でのボタン離したときの挙動を書く
  if(...){
    state = new ASate();
  }
};
BState.prototype.toString = function(){
  return "BState";
};

A/B各状態のonPressには別々の処理を書きます。
状態遷移するにはstateに遷移先のクラスをnewして引き渡してあげます。
ここまででステートパターンの定義はOKです。


次に例えばボタンのイベントハンドラなどに挙動を書きます。
その際にstateオブジェクトにあるonPressやonReleaseなどを呼び出すようにしてあげます。

button.on('press', function(){
  state.onPress();
});

button.on('release', function(){
  state.onRelease();
});

stateは状態によって差し替わるため、イベントハンドラ内のコードを切り替えずに制御を変更させることが可能となります。このようにすることで大量のif/switch文を書くことから開放されスパゲティコードになりにくくなります。
各状態は抽象クラスを継承しているため、挙動を書き忘れたところがあったとしても抽象クラスの動きが実行されることになります。抽象クラスにコンソールメッセージなどを出力するようにしておくと、どこが書き忘れたのかが一目瞭然になります。
また、状態を追加/削除したいといった場合でも新しいStateクラスを定義するだけなので全部のif/switch文を直さなくて良くなります。便利ですね。

ステートパターンを利用したデモプログラム

Tesselにオンボードで付いている4つのLEDを使い、ステートパターンを体感できるデモを考えました。仕様としては次のような感じです。

  • 10秒ごとに赤LED点滅、青LED点滅、緑LED点滅の状態が切り替わる。
  • 各状態でコンフィグボタンの挙動が異なる。
    • 赤点滅時:オレンジLEDをオン/オフを切り替えるトグルボタン
    • 青点滅時:無効
    • 緑点滅時:押している間だけオレンジLEDが点灯する
var tessel = require('tessel');
var util = require('util');

var state = null;// 状態の代入先
var loop = null;
var cnt = 1;//タイマー用


//各LEDをアクセスしやすいようにオブジェクトにセット
var led = {
    red : tessel.led[2],
    amber : tessel.led[3],
    green : tessel.led[0],
    blue : tessel.led[1]
};

//LEDの初期状態は消灯
led.red.output(0);
led.amber.output(0);
led.green.output(0);
led.blue.output(0);


// *****ステートパターン*****
// 状態遷移関数の定義
var transitionState = function(newState){
    console.log(state + " -> " + newState);
    //state変数を新しい状態にする。
    state = newState;
}

// AbstractStateクラスの定義
function AbstractState() {};
AbstractState.prototype = {
    press: function(event){
        console.log("Abstract press");
    },
    release: function(event){
        console.log("Abstract release");
    },
    loop: function(event) {
        console.log("Abstract loop");
    },
    toString: function() {
      return "AbstractState";
  }
};

//赤LED点滅状態
function RedState(name){
  AbstractState.call(this, name);
}
util.inherits(RedState, AbstractState);//継承
RedState.prototype.press = function(event){
    //ボタン押すごとにAmberのLEDを点灯/消灯切り替え
    led.amber.toggle();
};
//releaseは無効
RedState.prototype.loop = function(event){
    led.red.toggle();
    if(cnt % 10 == 0){
        //ループで10回カウントしたら青点滅の状態へ遷移
        transitionState(new BlueState());
    }
};
RedState.prototype.toString = function(){
    return "RedState";
};

//青LED点滅状態
function BlueState(name){
  AbstractState.call(this, name);
}
util.inherits(BlueState, AbstractState);//継承
//press,releaseは無効
BlueState.prototype.loop = function(event){
    led.blue.toggle();
    if(cnt % 10 == 0){
        //ループで10回カウントしたら青点滅の状態へ遷移
        transitionState(new GreenState());
    }
};
BlueState.prototype.toString = function(){
    return "BlueState";
};

//緑LED点滅状態
function GreenState(name){
  AbstractState.call(this, name);
}
util.inherits(GreenState, AbstractState);//継承
GreenState.prototype.press = function(event){
    //ボタン押すごとにAmberのLEDを点灯/消灯切り替え
    led.amber.output(1);
};
GreenState.prototype.release = function(event){
    //ボタン押すごとにAmberのLEDを点灯/消灯切り替え
    led.amber.output(0);
};
GreenState.prototype.loop = function(event){
    led.green.toggle();
    if(cnt % 10 == 0){
        //ループで10回カウントしたら青点滅の状態へ遷移
        transitionState(new RedState());
    }
};
GreenState.prototype.toString = function(){
    return "GreenState";
};


//*****メイン*****
state = new RedState();

//Tesselのコンフィグボタンを押したときのイベント
tessel.button.on('press', function(){
    state.press();
});

//Tesselのコンフィグボタンを離したときのイベント
tessel.button.on('release', function(){
    state.release();
});

//ループ関数
loop = setInterval(function(){
    //現在state変数に代入されているloop関数を実行
    state.loop();
    console.log(cnt++);
} ,1000);

デモプログラム実行

赤・青・緑点滅時にコンフィグボタンを4回押しています。それぞれの状態によって「トグル・無効・押しているとき光る」と動作が切り替わっています。
※動画が荒れ荒れになってしまったのはご了承ください。


電子工作で作るものはセンサの検出結果から状態変化することも多いと思うので、ステートパターンを利用すると効率よく開発ができるのではないでしょうか。