#あらすじ
イベント駆動の実装はたくさんあるけど、JSならもっとお手軽に実現する方法があるはずだと思ったので実験してみたお話。
#プロローグ
『ハイジャックされた飛行機内で怪我人発生!CAさんの「お客様の中にお医者様はいらっしゃいませんか!?」の問いかけに颯爽とブラックジャックが応える。「私は医者だ。無免許医だがね。」』
クライアントサイドでもNodeでもイベント駆動の実装はあるが、できればもっとお手軽に実現したい。
class CabinAttendant{
findDoctor(){
console.log("お客様の中にお医者様はいらっしゃいませんか!?");
dispatch("doctorPlease"); //"doctorPlease"イベント発信
}
}
class BlackJack{
//"on"から始まる関数は自動的にイベント受信関数として捉えられる
onDoctorPlease(){
console.log("私は医者だ。無免許医だがね。");
}
}
理想としてはカスタムイベントだの、EventEmitterを作成することなく、「dispatch(イベント)」とすれば「onイベント」メソッドを持つオブジェクトが反応するぐらいのお手軽さがいいのだ。
#いきなり要件
- 「dispatch(イベント)」を実行すると、該当する全てのオブジェクトの「onイベント」メソッドが実行される
- イベント駆動ヘルパー(CustomEventやEventEmitter)オブジェクトの作成は不要とする
- タイトル通り10行以内で実現すること
#そして実装へ
JavaScriptは関数名がわかっていれば文字列で関数を呼べるのはご存知の通り。それを利用すれば「イベント名」さえわかれば、イベント名に対して定型的に名称が決まる関数を呼び出すことなんてわけない。
dispatch = function(event_name, obj){
obj["on" + event_name]();
}
最初の要件はこれで片付いたも同然だ。
二つ目の要件だが、通常はヘルパー内にイベント駆動のたねや仕掛けを埋め込むことでイベント駆動を実現する。ヘルパーを利用しないとなると、親クラスにタネと仕掛を埋め込むぐらいしかない。
#イベント駆動の実現
イベント駆動の基本モデルはイベントを発生させる「Producer」(この場合は、CabinAttendant)、イベントに反応する「Consumer」(この場合は、BlackJack)、そして両者間でイベントを媒介する「Processor」からなる1。
概念的にはProducerとConsumerは別のクラスであるが、設計レベルで考えると「口しかない人(発信)」と「耳しかない人(受信)」なんて定義は不毛だ。
ブラックジャックの言葉に反応して同乗していた別の医師が『無免許医などに任せられるか!私に任せてもらおう』と割り込んでくるシナリオを想定した場合、ブラックジャックはイベントの受信(on)と発信(dispatch)を併せて行うはずだ。
class CabinAttendant{
findDoctor(){
console.log("お客様の中にお医者様はいらっしゃいませんか!?");
dispatch("doctorPlease");
}
}
class BlackJack{
onDoctorPlease(){
console.log("私は医者だ。無免許医だがね。");
dispatch("hasNoLicense");
}
}
class AnotherDoctor{
onHasNoLicense(){
console.log("無免許医などに任せられるか!私に任せてもらおう。");
}
}
設計上はProducerもConsumerも同じクラス(EventAwareクラス)で表すのが良い。
ProducerやConsumerが劇の登場人物だとすれば、最後に残ったProcessorは舞台の役割を担っている。人物と舞台は明らかに異質だ。当然別々のクラスで定義した方が良いだろう。
しかしclass宣言だけでも行数を消費してしまう。2クラスなら最低4行は消費してしまう。これでは「タイトルに偽りあり!」…と、なりそうなので、今回に限って抜け道を探してみよう。Processorに注目してその特徴をよく見ればきっと抜け道はある。
- singleton(メモリ上に常に1つだけ存在するオブジェクト)である
- 関数は持たず、EventAwareのリストを格納する機能しか持たない
これならばstaticで代替2できそうだ。staticの配列を用意すれば事足りる。
これで仕掛けは整った。
#10行でできます
ということでEventAwareさえ実装してしまえば、イベント駆動プラットフォームは完成だ。
class EventAware{
constructor(){
(EventAware.objectArray = EventAware.objectArray || []).push(this);
}
dispatch(event, ...args){
let funcname = "on" + event[0].toUpperCase() + event.slice(1);
EventAware.objectArray.forEach(o => o !== this && o[funcname] && o[funcname](...args));
}
}
staticプロパティは「クラス名.プロパティ名」で宣言できる。ここではEventAwareクラスのコンストラクタが初めて呼ばれた時にstaticな配列(EventAware.objectArray)も同時に用意することにした。併せて今まさに作成しているEventAwareオブジェクトを配列に格納している。
後はdispatchの実装だ。まずはイベント名に紐づく定型的なイベント受信関数名の特定だが、今回は「"on" + イベント名の先頭文字を大文字にした文字列」で行こう。過去にNewしたオブジェクトは全て配列に格納されているので、配列内のオブジェクトが「イベントを発信したオブジェクトとは異なるオブジェクトで」、「イベント受信関数が実装されていれば」、「その関数を実行する」で完成。
利用方法はこの通り。
class CabinAttendant extends EventAware{
findDoctor(){
console.log(this.constructor.name + ": お客様の中にお医者様はいらっしゃいませんか!?");
this.dispatch("doctorPlease");
}
}
class BlackJack extends EventAware{
onDoctorPlease(){
console.log(this.constructor.name + ": 私は医者だ。無免許医だがね。");
this.dispatch("hasNoLicense");
}
}
class AnotherDoctor extends EventAware{
onHasNoLicense(){
console.log(this.constructor.name + ": 無免許医などに任せられるか!私に任せてもらおう。");
}
}
var ca = new CabinAttendant();
var bj = new BlackJack();
var doc = new AnotherDoctor();
ca.findDoctor();
CabinAttendant: お客様の中にお医者様はいらっしゃいませんか!?
BlackJack: 私は医者だ。無免許医だがね。
AnotherDoctor: 無免許医などに任せられるか!私に任せてもらおう。
#Mixinとの併用
親クラスがEventAwareに縛られことが気になる人は、各種Mixinとの併用をお勧めする。拙作のMixinライブラリを紹介した記事をご一読いただければ幸いである。
#最後に
今回は最小コード行で利用しやすいイベント駆動プラットフォームを実現する実験実装だ。もし本記事を参考にしていただけることがあれば、コードをそのまま利用するのではなく、より良い実装を目指して欲しい。
なお本記事で作成したコードは(記事内ではコメント表記していた箇所も含めて)githubからダウンロードできるので興味がある方はダウンロードしていただきたい。
-
著名なObserverパターンは、「Producer(ObserverパターンではSubject)」が「Processor」を兼ねて、直接「Consumer(ObserverパターンではObserver)」にイベントを通知する形式をとっている。個人的にはProcessorが欠けるとイベント駆動のそもそもの目的である「アプリケーションレイヤーレベルオブジェクトの疎結合」が実現できなくなるため、ProducerがProcessorを兼ねる形式での実装はまずいと思う ↩
-
staticとsingletonは相互代替できるケースが多い。特にJavaScriptの場合、staticに課せられる実装上の制約は何もないに等しく、安直に利用しやすい。しかし可読性の観点からは大いに違う。本質的に同時に扱うべきプロパティだけをクラスとして集約することで、「拡張しやすく、変化を受けにくい」(いわゆるオブジェクト指向設計原則に従った)良い設計が可能になるし、同時にコードの可読性も高くなる。staticを扱う場合は「そのstaticは本当にそのクラスに存在するべきなのか」を常に問うて欲しい。今回のように異質なもの同士を「コードが節約できるから」という理由で同居させるのは設計上はNG行為だ。だから今回の設計は「抜け道」設計なのである。 ↩