CRAPモデルの概要
CRAPモデルについて
概要
オブジェクト指向的概念のMVCをメッセージ駆動概念に置き換える試み。
- (C)「コンダクター(conductor)」
- メッセージを調整して各ユニットと授受する指揮者
- (R) 「レセプター(receptor)」
- 画面からの入力を受け取りコンダクターに受け渡す
- (A) 「アクター(actor)」
- コンダクターの指示で各種の処理を行う
- (P) 「パフォーマー(performer)」
- コンダクターの指示で画面表示を行う
以上の各ユニットから構成される。
各ユニット間の関係
receptor → conductor → performer ⇅ actor(s)
各ユニットの役割
コンダクター(conductor)
指揮機能。後述する各ユニットとの通信を指揮する。各種イベントの制御を行う。後述のレセプターやアクターとの通信等で発生するイベントを制御する。非同期処理の競合を調整したりするのも役目になると思う。調整されたイベントを後述のアクターやパフォーマーに通知する。
レセプター(receptor)
入力機能。主に画面入力(DOM)で発生するイベントを受け取りコンダクターに通知する。
コンポーネントのネスティング
再生産性を考慮し、コンポーネントはより小さなコンポーネントを組み合わせ、それに付加機能をつける形での開発を想定する。
- 各コンポーネントは独自のCRAPを持つ。
- 子コンポーネントは親コンポーネントから独立しており、その挙動はブラックボックスとなる。
- 子コンポーネントは、親コンポーネントに必要な情報のみ通知する。
- 親コンポーネントは、子コンポーネントに必要な情報を通知する。
- 親コンポーネントから見ると、子コンポーネントのコンダクターが自分のアクターとなる。
例)
の場合。
「メニューバー」にカーソルがある時の処理は「メニューバー」自身が行えば良い。
プルダウンメニューの表示やメニューアイテムの選択イベントの受信、選択されたアイテムの取得等の機能は「メニューバー」単独で解決できる。
「メイン画面」はそれらを知る必要はなく、また干渉するべきではない。
「メイン画面」は例えばメニューから選ばれたアイテム選択の結果さえ得られれば良い。
これは「メニューバー」(のconductor)から通知を得れば足りる。
各ユニットが稼働するスレッド
役割 | スレッド | 概要 |
conductor | メイン+WORK | 各ユニットのイベント通知(メッセージ)を受け付けて非同期に稼働する役割なので、主にメインスレッドとは別のスレッドで稼働する。後述するがperformerがメインスレッドで稼働する関係上、一部機能はメインスレッドに置く |
receptor | メイン | UIによるイベントはメインスレッドで発生するので、メインスレッドで稼働する |
actor(s) | メインorWORK | メインスレッドで動かすには負荷が大きすぎるものは別スレッド化する。また、バックエンド・サーバーとの通信など待ち合わせが発生する処理も別スレッド化する。ライトウェイト・コンポーネントなど処理が軽くてスレッドを生成するコストが高く付く場合はメインスレッドで動かす |
performer | メイン | 出力機能としてはDOMやCSSOMを操作しなければならないが、JavaScriptの仕様上、メインスレッドでしかこれらを扱えないので、メインスレッドに置く |
がらくたにおける再生産性の雑感3
コンポーネント対応の件、なんとなく考えがまとまってきた。
コンポーネントと一口に言ってもライトウェイトなモノとヘビーウェイトのモノがある。そこには留意する。
しかしまあ、画面部品のほとんどはライトウェイトなものだ。これらはRAPの全てをメインスレッドに置くことになるだろう。
で、conductorとのやり取りも(メッセージの形で隠蔽するとは言え)イベントリスナーやコールバック関数を使って素早い呼び出しをするだろう。
ところで、こうなるとconductorにはたくさんのコンポーネントのRAPを捌く機能が要るわけだが、conductorから見ると「どのコンポーネントのreceptorから来た通知か」とか「どのコンポーネントのperformerに通知を渡すのか」と言ったオーバーヘッドが大きくなる。素早い呼び出しを志しても無意味になる。
各コンポーネント専用のconductorが有れば、比較的簡単に済む判断が、中央の単一のconductorが扱うと複雑になる。
結局はコンポーネント毎にconductorを持つ方が都合が良さげ。コンポーネントはRAPじゃなくCRAPを持つ。
「子コンポーネント」を組み合わせて構成された「親コンポーネント」なら、各「子コンポーネント」のconductorが「親コンポーネント」のconductorの子部品となるわけだ。
「親コンポーネント」にも独自のRAPが有るだろうが、「子コンポーネント」のreceptorが受け取ったイベントは「親コンポーネント」のreceptorが気にする必要はないってか気にするべきじゃないし、「子コンポーネント」のperformerが操作するDOMツリーは「親コンポーネント」のperformerはアンタッチャブルであるべきだ。
「親コンポーネント」のCRAPから見て、子コンポーネント」は独立してブラックボックス化されるべきだから、これでいい。
「親コンポーネント」からすれば「子コンポーネント」と必要なメッセージの授受が出来れば良いので、「親コンポーネント」のconductorと「子コンポーネント」のconductorがメッセージを通じて連動出来れば良い。
例えば「メイン画面」が親コンポーネントで、「メニュー部品」が子コンポーネントなら、メニューにカーソルがある時の処理は「メニュー部品」が考えれば良くて、「メイン画面」は例えばメニューから選ばれたアイテム選択の結果さえ得られれば良い。これは「メニュー部品」(のconductor)から通知を得れば問題ないわけだ。
とりま、このラインで考えよう。
がらくたにおける再生産性の雑感2
まあコンポーネントを組み合わせることによるreceptorやperformerの複数化は、conductorとやり取りする部品だけ単一化すれば別に問題ないな。
receptor 1 -+- joint → conductor receptor 2 -+ receptor n -+
conductor → joint -+- performer 1 +- performer 2 +- performer 3
第1感はこんなイメージ。
実際にはコンポーネントは入れ子構造を持つだろうから、こんな風に水平展開出来るか考えないとだけど、ね。素直にツリー型の構造にして、結合部毎にjointを作るのかもしれない。
この辺は実際にテストしてみて、負荷とか考慮しないと決められないかもね。
がらくたにおける再生産性の雑感
再生産性というか、普通に生産性を考えれば、アプリをスクラッチから作るばかりじゃなく、個別に開発した小さなコンポーネントを組み合わせてより大きなコンポーネントを開発したり、ライブラリーの類いを利用したり出来ないと意味がない。
(画面表示や入力に依存しない)actorだけで完結するライブラリーなら話は簡単だ。
が、DOMを操作するようなjQuery的ライブラリーを利用できるようにするのは面倒だなぁ。performerでのみ利用できる様に制限することが出来るだろうか。てか、jQueryだとreceptor関係も操作できるから、そもそもperformerにも導入出来ないか。ツラたん。
画面構成部品のコンポーネント利用は生産性を考えれば絶対に必要だな。
しかし、コンポーネントとなると、例えばプログレスバーとかリストアイテムのセレクターとか、インテリジェンスな部品だから、それぞれのコンポーネントが「独立」したRAP(receptor、actor、performer)を持つわけだ。
- メインスレッドに同居する各コンポーネントのRAP間の「独立」の保証(連動するけど干渉しない)
- actor(s)について、起動のコストやリソースの消費が問題になるworkerを増やさずに、上手に同居させるメカニズム
- receptor(s)やperformer(s)をコンポーネント毎に作る場合、conductorとのやり取りの複雑化
よく考えないと利用方法がややこしくなったり、処理効率が非現実的になったりしそう。
気をつけねば。
雑感2
一番レスポンスが要求されるであろう、receptor → conductor間の通信と、conductor →performer間の通信を考えると、conductorもメインスレッドに置いて、イベントハンドラにコールバック関数を登録する形にするのが一番コストが低い。
が、conductorはactorたちの非同期処理をコントロールする都合上promise周りで待合わせたりするだろうから、下手を打つとメインスレッドを止めてしまわないか心配なんだよねー
conductorは、receptorやperformerと(コールバック関数で)連動するメインスレッド部分と、actorや別のconductorと通信する別スレッド部分に分けて実装するのが正解なのかもしれないな。
actorだって、TypeScriptでササっと書いただけの軽い処理なら別スレッドにする必要無いし、その辺を使い分けられればそれに越したことはない。実装が複雑になるから、そんな機能は後回しだけどw