流れるようなインタフェースを持つ Command

流れるようなインタフェースを持つ Command クラスを書いてみた。実装のポイントは以下の 2 点。

  • Command 自体にキューイングの機能を持たせた。
  • Composite パターンを適用した。

実行するとこんな感じになる。

普通に数珠つなぎで、

var hoge:Hoge = new Hoge();//1000ms wait する Command
var foo:Foo = new Foo();//0ms wait する Command
var bar:Bar = new Bar();//2000ms wait する Command
var baz:Baz = new Baz();//3000ms wait する Command
hoge.next(foo).next(bar).next(baz).execute();
=====start=====
[object Hoge] execute
[object Foo] execute
[object Foo] done
[object Bar] execute
[object Bar] done
[object Baz] execute
[object Baz] done
[object Hoge] done

逆にして、

foo.next(baz).next(bar).next(hoge).execute();
=====start=====
[object Foo] execute
[object Baz] execute
[object Baz] done
[object Bar] execute
[object Bar] done
[object Hoge] execute
[object Hoge] done
[object Foo] done

Foo に Bar と Baz をネストして Bar と Baz を同時実行で、

hoge.next(foo.next(bar, baz)).execute();
=====start=====
[object Hoge] execute
[object Foo] execute
[object Bar] execute
[object Baz] execute
[object Bar] done
[object Baz] done
[object Foo] done
[object Hoge] done

done のタイミングが梱包状態になっているのは流れていないとも言える。完了のイベントを受け取りやすいのと、中断処理を実装しやすかったのでそうした。

以下、ソースコード

package {
import flash.events.EventDispatcher;
import flash.events.Event;
/**
*  コマンドクラス
*  @author tanablog at gmail dot com
*/
public class Command extends EventDispatcher {
private var children:Array;
private var processingChildren:Array;
private var isProcessing:Boolean;
private var parallelCount:int;
/**
*  コンストラクタ
*/
public function Command() {
children = new Array();
processingChildren = new Array();
isProcessing = false;
}
/**
*  追加する
*  @param ...children 次に実行するコマンド
*  @return 自分自身
*/
public function next(...children):Command {
if (!isProcessing) {
this.children.push(children);
}
return this;
}
/**
*  実行する
*/
public function execute():void {
trace(this, "execute");
isProcessing = true;
}
/**
*  停止する
*/
public function cancel():void {
if (!isProcessing) {
return;
}
while (processingChildren.length > 0) {
var childGroup:Array = processingChildren.shift();
for each (var child:Command in childGroup) {
child.removeEventListener(Event.COMPLETE, executeChildren);
child.cancel();
}
}
trace(this, "cancel");
isProcessing = false;
dispatchEvent(new Event(Event.CANCEL));
}
/**
*  子要素を再帰的に実行する
*  @param event Eventインスタンス
*/
private function executeChildren(event:Event = null):void {
if (event != null) {
event.target.removeEventListener(Event.COMPLETE, executeChildren);
}
if ((parallelCount - 1) > 0) {
parallelCount--;
} else {
if (children.length > 0) {
var childGroup:Array = children.shift();
parallelCount = childGroup.length;
for each (var child:Command in childGroup) {
child.addEventListener(Event.COMPLETE, executeChildren);
child.execute();
}
processingChildren.push(childGroup);
} else {
done();
}
}
}
/**
*  終了した
*/
protected function done():void {
//for top level command
if (children.length > 0) {
executeChildren();
} else {
trace(this, "done");
isProcessing = false;
dispatchEvent(new Event(Event.COMPLETE));
}
}
}
}