前に一度org.libspark.threadライブラリについてエントリーを書きましたが、このライブラリは本当に素晴らしいです。フレキシブルに使えるし、使っているだけで自然とOOPの作法が身に付いていくような気もします。
なんでこんなに使い心地がいいのかつらつら考えているのですが、いまだうまくエントリーにまとめられそうにありません。が、
とりあえず、今回は「Threadは新しいデザインパターンの実装なのではないか」という大胆な仮説を唱えてみようかと思いますw
まずは 同期処理、非同期処理のおさらい
さて、タスクAのあとタスクBを実行したいという状況があるとします。
以下のようなコードからスタートします。
package
{
import flash.display.Sprite;
public class AboutThread extends Sprite
{
public function AboutThread()
{
/**
* タスクAが終わった後タスクBをすませたい
*/
doTask();
}
//タスク実行
private function doTask():void
{
//タスクAを生成
var a:A = new A();
//タスクを実行
a.execute();
//タスクBを生成
var b:B = new B();
//タスクを実行
b.execute();
}
}
}
とってもシンプルで清々しいコードですね。何の問題もありません。平和な世界。
ただ現実はこのように甘くはないかもしれません。
タスクAは非同期なタスクかもしれないからです。
もし非同期の場合は、タスクの終了を通知してもらう手続きをして、さらに通知を受け取るイベントハンドラも必要になりますね。
あと、一度だけ通知してもらいたい場合(よくある!)は、ハンドラ内で通知解除の手続きも必要です。
コードは以下のような感じです。
package
{
import flash.events.Event;
import flash.display.Sprite;
public class AboutThread extends Sprite
{
public function AboutThread()
{
/**
* タスクAが終わった後タスクBをすませたい
*/
doTaskA();
}
//タスクAを実行
private function doTaskA():void
{
//タスクAを生成
var a:A = new A();
//タスク終了を通知してもらう
a.addEventListener(Event.COMPLETE, taskACompleteHandler);
//タスクを実行
a.execute();
}
//タスクAが終了した
private function taskACompleteHandler(event:Event):void
{
//リスナの登録を解除
A(event.target).removeEventListener(Event.COMPLETE, taskACompleteHandler);
//タスクBスタート
doTaskB();
}
//タスクBを実行
private function doTaskB():void
{
//タスクBを生成
var b:B = new B();
//タスク終了を通知してもらう
b.addEventListener(Event.COMPLETE, taskBCompleteHandler);
//タスクを実行
b.execute();
}
//タスクBが終了した
private function taskBCompleteHandler(event:Event):void
{
//リスナの登録を解除
B(event.target).removeEventListener(Event.COMPLETE, taskACompleteHandler);
//...続く
}
}
}
突然コード量が増えてしまいました。ざっと倍くらいでしょうか。
何が問題なのか?
単純にコードの量が増えたことを問題にしているわけではありません。
最近の気の利いたエディタを使っていれば半自動でリスナ登録やハンドラの記述をやってくれますもんね。
コードを書くときはそれほど負担にはならないんです。でも、コードを読むときはどうでしょうか?
本来書きたかった処理は、「タスクAのあとにタスクBを実行したい」という物だったはずです。
もちろん残念ながらコンパイラは自然言語で書かれた命令のような抽象的なことを理解することはできないので(死ぬまでにはそんなコンパイラが出てくることを望みますが)、
それなりの手続きを記述していく必要があります。
問題にしているのは手続きの細かさです。
上記のコードは、「タスクAのあとにタスクBを実行したい」ということを記述したいはずのコードなのに、その一段低レベルなイベントシステムの手続きという層がむき出しになっています。
また、これが、「Aのあと、Bのあと、Cのあと、Dのあと、Eしたい」の様なケースではイベントの手続きという低レベルなコードがほとんどを全体のほとんどをしめてしまうでしょう。
Threadはこれを解決する?
はい。解決します。
このように
package
{
import org.libspark.thread.Thread;
public class AboutThread extends Thread
{
public function AboutThread()
{
/**
* タスクAが終わった後タスクBをすませたい
*/
doTaskA();
}
//タスクAを実行
private function doTaskA():void
{
//タスクAを生成
var t:Thread = new DoAThread();
//タスクを実行
t.start();
//タスクの終了を待つ
t.join();
//終わったらタスクB開始
next(doTaskB);
}
//タスクBを実行
private function doTaskB():void
{
//タスクBを生成
var t:Thread = new DoBThread();
//タスクを実行
t.start();
//タスクの終了を待つ
t.join();
//終わったら次の...
next(doSomething);
}
}
}
コード量も減りましたが、何より嬉しいのは上から流れるように処理を記述できる点です。
イベントという仕組みは一見隠蔽され、一段抽象的なレベルで処理を記述できています。直感的とも言えますね。
start()やnext()などとても直感的な記述で、もし初めてThreadを使ったコードを見た人でも何をやっているか想像できるんじゃないでしょうか?
t.join()というちょっと違和感を感じるかもしれない記述がありますが、これは「tの終了を待つ」と読み替えてもらうといいです。
ちょっと脱線
個人的にはjoin()という名前に違和感を感じる。startやnextと意味の階層が違う気がしてて、うまく言えないけど利用者視点じゃなく設計者視点な名前という感じで。
これがもし、waitForFinishing()とか(ださいけど)だと意味がつながる気がする。さらに、join()とnext()をまとめたシンタックスを用意して、
さらにさらに流れるようなインターフェイスで、t.start().andThen(func)とか書けると最高だー。
非同期処理のコード大盛り問題を解決するだけの物ではない
本題です。
最近、自分はコントローラはすべてThreadで書いています。同期、非同期関係なくです。
たとえ同期なタスクでもjoin()してしまいます。そうすると何が嬉しいのかと言うと、
もし、コードを書き始めた当初同期だったあるタスクがあとから非同期になっても、クライアントコードはいっさい変更する必要がありません。
いいかえれば、クライアントはそのタスクが同期か非同期かを意識する必要なく処理を記述できるんです。
「AしてBしてCしてDして…」
それらのタスクが同期なタスクか非同期なタスクかいっさい気にかけることなく流れるように処理を記述でき、後から読むときも流れるようにコードを読めます。
エントリーの最初にあげた大胆な仮説、「Threadは新しいデザインパターンの実装なのではないか」というのはこれです。
「同期か非同期かを意識する必要なく処理を記述する」パターン。
ThreadはCommandパターンとCompositeパターンの応用だとは思うんですが、そのどちらにもない同期、非同期性の隠蔽という機能があるんですよね。
少なくともGoFの23のパターンにはないパターンだという気が。。
こういうパターンに既に名前がついているかどうか知っている人がいたら教えてください〜。
と書いては見たものの
うーん、新しいパターンはちょっと言い過ぎたかも。やっぱりw
ほとんどのCommand系のライブラリは非同期なタスクをサポートしてますもんね。
でも、いろいろあるCommand系のライブラリの中でも、クライアントコードの記述の簡潔さは一歩抜きん出ていると思います。
僕も最近、Threadライブラリにお世話になって感激していたところです。
リリース当初はaddEventListenerにそれほど違和感を感じてなかったのですが、使い始めたらこのライブラリ無しにはいられなくなりますよね。
join()という名前はJavaのThreadに倣ってそういうネーミングなのだと思います。