AvalonからMVVM、そしてRxへ: GUIプログラミングの哲学の歴史

MSテクノロジ知らんがな、とよしぞうに言われて、そういえばこの辺の話は外ではあまり聞かないな、と思ったので、ちょっと軽く振り返ってみる。
なお、Javaプログラマ向けに一部翻訳してるので、C# の実際とはちょっと違う。

かつて人々は、onclickでリクエストを発行しデータを取ってきて、その間はローディング中としてアイコンを回したりして、帰ってきたらアイコンを戻して取得したデータからtableを組み立てたりしていた。
このシーケンシャルな手続きプログラムは、非同期なGUIという物と大層相性が悪く、すぐにアイコンが回り続けたり途中で何か違う事をすると落ちたりといったバグを埋め込んでしまい、人々は悩んでいた。

GUIプログラムのバグはどこから来るのだろうか?
それはページの動的な所から来る、という観察があった。
静的なhtmlはあまりバグらない。
一旦動く、という事が静的に確認されれば、それ以後はバグらない。

でも静的なページだけでは目的の機能は達成出来ない。
そこで静的なページに一部、動的な物を混ぜよう。
でもその混ぜ方を無制限にしてしまうと、JSPのようになってしまって、結局バグが入ってしまう。
そこで限られた形で、動的な部分は最小限にしたい。
そして理想的にはそれは、静的なチェックが通ればバグフリーだと嬉しい。現実的にはそれは難しくても、なるべくそれに近い方が良い。
そんな「準静的」を目指す。それがAvalonという名のプロジェクトの目指した物だ。

Avalon のチームは、GUIプログラムの動的な要素というのは、パターンがあると思った。
全体としては動的でも、それぞれの状態という概念があって、それが遷移し、それぞれの状態に対して一つの静的なページが対応する、という形で大部分が表現出来るんじゃないか?と考えた。
その形式に合わない物も多少はあるだろうけど、だいたいは出来る。で、そのだいたいは準静的に書けるようにしよう。それ以外の物はこれまで同様、プログラムで書くしか無い。

さて、状態とは何か?というとオブジェクトで良いだろう。
あるオブジェクトの持つデータの値が決まると、そこから一意にページが決まる。
でもこれだけだとユーザーの操作に反応出来ない。
そこでコマンドと言う物も作られた。
基本的には何かアクションがあると、コマンドという物が実行される。
そして、その結果オブジェクトの値が変わり、その値に応じてまたページが生成される、というルールにした。
別にコマンドの中で直接UIを変更する事は出来るのだけど、やらない方が良いスタイルだ、と言う事にした。

哲学的にはそれで終わりだが、実装的にはUI 側がオブジェクトの変更を知る必要がある。
値の変更をどう伝えるかという事でINotifyPropetyChangedというインターフェースが作られた。なんて事はなくて、変更されたフィールドは
OnPropertyChanged(object target, string propName )
を呼ぶ、という紳士協定。
フィールド名の文字列。ださい。型とか無い。
で、XAML側はこのコールバックを設定して、呼ばれたらリフレクションで値とってコントロールの値を変更する。
ICommandは普通のOnClickListenerと本質的には変わらないので今回は説明しない。むしろ大切なのは、このモデルに沿ってUIプログラムを書く、というガイドラインだ。
ICommandの先で直接getElementByIdして値変えたりはしない、という推奨に従う。
状態を更新してOnPropertyChangedを呼ぶ、というガイドラインになるべく従ってくださいね、という事。
つまり通常のGUIプログラミングを

1. コールバックに反応して状態を変更するコード
2. 状態に応じてページを生成する部分

の2つに分ける、というプログラミングスタイルで書きましょう、と言った。
そして2に関しては準静的に書けるようなDataBindという拡張シンタックスがある。
2はうまく作ればほとんどバグらないように作れる。1はこれまで通りバグが入るが、GUI特有のバグはかなり駆逐出来た。
これがAvalonだ。

ちょっと準静的、という事について説明を補足しておく。
静的なページを通常の型チェックのあるプログラムに例えると、準静的とはJavaのGenericsに近い。
大部分は静的に書いて、そこはチェックされるが、一部Generics引数としてそこに穴が空いている。
ただ穴は無制限になんでも入るのでは無くて、ある種の制約がある。
実際のページは状態に応じてその場その場で特殊化される。特殊化された物はほぼ完全な静的なページと言って良い。実行時に特殊化されるのだから静的にチェックされるGenericsとはちょっと違う。当然特殊化してみないと分からないバグはあるから、静的なページが生成されてみないと分からないバグは残る。つまりRuntimeExceptionは避けられない。
ただ、ある種の制約はあるので、一つが動けばだいたいは全部動くので、見た目程は違わない。
準静的に書ける物としては、基本的には値のマッピングと、対象となるオブジェクトの型に応じたパターンマッチくらい。例えばテーブルやリストを表示する時に、対応するIEnumerableの要素の、型に応じてディスパッチくらいは出来る。
if文とかは書けないので、値をVisibleにマップして擬似if文みたいな事はする。
とにかく、宣言的に書けそうな範囲でGenericsが使えるような物と思っておくと、だいたい正しい。なおGenerics引数は正式にはBinding式と呼ばれていて{}で書く。

さて、実際にAvalonが世に出てみると、意外とそううまくは行かなかった。
UIの変更の内容が下のオブジェクトとそんなに綺麗にマップされない。
このオブジェクトのこのプロパティとこのオブジェクトのこのプロパティの両方の値を見て、これこれならこのテキストボックスをenable、とか結構あるが、テキストボックスごとにそれらをラップするオブジェクトを作ってセットするんじゃ、自分で普通に手続き的にUI 書くより大変になってるよ!

コントロールもちゃんとデータの内容からだけで毎回生成するように書くのはかったるくて、適当にコード側と癒着させる方がずっと楽だった。
そこで皆割とこれまで通りのGUIプログラムを書いてた。
GUIを準静的に書く、というのは、思想はご立派だが実際にはめんどくさいよねぇ、というのが当時の結論と言ってよかろう。
MSから提供されてるデータのクラスやコントロールのクラスを使う時には準静的に書くけど、それ以外のカスタムな所では皆これまで通りのコードを書いていた。
それにしてもなんか標準のコントロールはゴテゴテしてて使いにくいなぁ。

時は流れて。
ゴテゴテしたコントロールは意外と柔軟性があり、結構自分でコードを書かなくてもいろんな事が出来る事が分かってきた。その範囲では準静的に書く事になるので楽に書ける。
でもその枠から少しはみ出すと全部自分で昔ながらのやり方で書かないといけない。これは凄いバグるし、ゴテゴテして面倒くさい物を触らないといけない。
もーやだ!何で全部を準静的に書けないんだ!
キレた人々は、これまでの「なるべく多くの物を準静的に書いて残りはこれまで通り書く」、というスタイルを捨てて、「何がなんでも全てを準静的に書く」という事で揃えるという方法を編み出した。
これまでプログラムで実現していた部分を無理やり全てINotifyで通知するプロパティを作り、そのやり方では無理そうな事でもとにかくなんとかこじつけてDataBindだけで実装する。

例えば、

window.SetCursor(Cursors.WAIT);
model.VeryLongOperatioe ();
window.SetCursor(Cursers.NORMAL);

みたいなのがあったとする。
ちなみに長いオペレーションをUIスレッドでは普通はやらないからそこは本当はasyncでちゃんと書くが、そこはどうでもいい。
こういう時も無理やりvmとか言うオブジェクトを作って、

vm.SetCusorValue(Cursors.WAIT);
model.VeryLongOperation();
vm.SetCursorValue(Cursors.NORMAL);

とvmという変数名のオブジェクトのメンバにセットする、という風に無理やりオブジェクトの状態の変化とUIの変化を揃える。
このただUIの状態を変更する為だけに存在するオブジェクトをViewModelと呼ぶ事にした。
このViewModelの実装で、

void SetCursorValue(Cursor cur) {
m_cursor = cur;
OnPropertyChanged(this, "CusorValue")
}

とかわざわざやる。
元のコードより無茶苦茶長くなっててめんどくさい!
でももう全部こうやって書く、と決めた。
しかもどうせいつかはUIの状態を思い通りにする為に、UIの状態に一対一に対応するだけのクラスが必要になる。
だから最初から全てを諦めて先回りしてそのクラスを作る、と決めた。心を無にして何も考えずにViewModelという名前の、そのUIと一対一対応したクラスを毎回作る。
全部UIのプログラムをそのクラスのメンバの値の変更だけで表現する、と勝手に自分ルールを定める。そんなViewModelを、各UI画面ごとに毎回作るという鉄の掟を定めた。
これをMV VMパターンと呼ぶ。
VMの定義は本当にかったるくて、普通に書けば簡単に出来る事をわざわざ倍くらいのコードにしてバカなんじゃないの?と思うけれど、そこは心を無にしてひたすら元々のUIのコードをVMに翻訳する作業を続ける事にした。みんなヤケになってた。もう考えるのに疲れた。何も考えずにひたすらVMに翻訳する。
VMへの翻訳は少しやってみると分かるが単純作業で何も頭を使わない。ひたすら面倒くさいが、心を無にしてやる。Excelの経費精算みたいなもんだ。

さて、ヤケになって、全部を無理やり準静的に書くと、毎回この作業を画面ごとにやる事になる。
何度も同じ単調作業を延々と繰り返すので、すぐに慣れて凄い速度でVMが作れるようになる。
みんな絶対この作業は人間がやるべきじゃない、と思っているのだが、もう慣れてしまった。まぁいつかは誰かが解決してくれるだろう、それまでは単調作業を続けよう。

結局Windowのメソットを呼ぶのとVMのメソッドを呼ぶのでは、同じじゃん!ってのは、半分正しいが半分間違ってる。
まず、このやり方では書けない、または書きにくいUIが結構ある。
例えばプログレスダイアログを出す、とかやりにくい。メッセージボックス出したりもやりにくい。
複数のWindowにまたがるのは苦手な事が多い。

つまり、ある種のサブセットに無理やり自分達のUI仕様を押し込めるスタイルとなっている。
このサブセットで実現出来ないUIを作りたい時はどうするのか?というと、簡単に出来るようなUIに頑張って翻訳する。
例えばダイアログの代わりにLoadingと書いてあるラベルをhiddenにしておいて、ローディング中はVisibleにする、とか。これならプロパティとのデータバインドで実現出来る。それだけだと冴えないのでアニメーションとかつけてそれっぽくする。

そういう良くあるUIの要求の、VMでの実現方法をみんなで考えて共有してった結果、最近はだいたい全部のUIの要求はMVVMに翻訳出来るようになってきた。
そしてだいたいは、既存の良くあるUIよりは良いUIに出来たので、理論武装も済んだ。何故良い物に出来たのかはまた別のストーリーがあるのだが、そこは今回は触れない。

さて、このVMで表現出来るサブセットに機能を翻訳する、というのがMVVMの根幹だ。
そのサブセットとは、準静的に書けるUIに制限する、という事でもある。
ただしAvalonが出た当初のような、出来る所だけ準静的にする、という姿勢よりは、解空間はずっと広い。各UIの変化だけを担当したプロパティを作ってプログラムをするのだから、理論上可能な準静的の全てを解空間と出来る。また、XAML自体の表現力も当初思われてた以上に高く、これまでのUIでは静的に表現出来なかった物もかなり静的に表現出来る事が分かってきた。これも解空間を広げるのに一役買った。
解空間が広がった事と組み合わせの試行錯誤のノウハウが溜まってきた事で、単なるヤケだったMVVMは、実際のUIの問題の多くを解決出来るようになってきた。

そもそもにバグりやすい物とは、準静的に書けない所が多かった。そこを意図的にUIから排除してしまったので、バグりにくくなったのは当然といえば当然である。バグりやすい仕様の代わりをみんなで探した、これがMVVMという物の正体だ。

さて、これでUIプログラムは全て準静的となり、UIにまつわるバグは激減した。
というよりバグるUIを書くのを辞めた、というべきかもしれない。
バグらないパターンだけでUIを構築するようになった。うまく行く事が分かってる物を組み合わせるだけでUIを作る。
静的なページが一旦表示されればバグらないのに似ていて、準静的なUIも一旦表示出来たら、かなりバグらないUIが作れるようになった。
その代わりのExcelの経費精算は受け入れる事にした。

これでUIからのバグはほとんど排除出来たのだが、幾つか新たな問題も生まれた。
まずはUIの値の変更の通知とモデルの受け取りたい通知のミスマッチ。
例えばTextがタイプされる都度Suggestを出す、とかの処理をMVVMで実装する場合、このTextを一文字打たれるごとにVMに通知が行くように書くのだが、このVM側のコードはかなり動的になってしまう。
入力が終わってFocusOutのタイミングやSubmitボタンが押されたタイミングで取るなら割と準静的なのだが、もう少し細かい事をやろうとした瞬間にキーのイベント一つ一つを受け取らないといけない。間が無い。

また、UIのバグはだいたい駆逐出来たのだが、UIスレッド以外の作業はコンソールアプリの同期時代に比べて難しくなってしまった。
例えばリクエストを投げて帰ってきたら何かやる、タイムアウトだったら何かやる、みたいなコードを非同期で書く必要があるのだが、これは非GUIプログラムの時に比べて非同期な度合いが上がってて難しい。

長くなり過ぎたのでRxの話はカット(ぉ

追記: 要望が多かったので簡単に続き書きました。 AvalonからMVVM、そしてRxへ(その2): GUIプログラミングの哲学の歴史