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はこれまで通りバグが入るが、G UI特有のバグはかなり駆逐出来た。
これがAvalonだ。
ちょっと準静的、という事について説明を補足しておく。
静的なページを通常の型チェックのあるプログラムに例えると、準静的とはJavaのG enericsに近い。
大部分は静的に書いて、そこはチェックされるが、一部Generics引数としてそこ に穴が空いている。
ただ穴は無制限になんでも入るのでは無くて、ある種の制約がある。
実際のページは状態に応じてその場その場で特殊化される。特殊化された物はほぼ完全な 静的なページと言って良い。実行時に特殊化されるのだから静的にチェックされるGen ericsとはちょっと違う。当然特殊化してみないと分からないバグはあるから、静的 なページが生成されてみないと分からないバグは残る。つまりRuntimeExcep tionは避けられない。
ただ、ある種の制約はあるので、一つが動けばだいたいは全部動くので、見た目程は違わ ない。
準静的に書ける物としては、基本的には値のマッピングと、対象となるオブジェクトの型 に応じたパターンマッチくらい。例えばテーブルやリストを表示する時に、対応するIE numerableの要素の、型に応じてディスパッチくらいは出来る。
if文とかは書けないので、値をVisibleにマップして擬似if文みたいな事はす る。
とにかく、宣言的に書けそうな範囲でGenericsが使えるような物と思っておくと、だ いたい正しい。なおGenerics引数は正式にはBinding式と呼ばれていて{ }で書く。
さて、実際にAvalonが世に出てみると、意外とそううまくは行かなかった。
UIの変更の内容が下のオブジェクトとそんなに綺麗にマップされない。
このオブジェクトのこのプロパティとこのオブジェクトのこのプロパティの両方の値を見 て、これこれならこのテキストボックスをenable、とか結構あるが、テキストボッ クスごとにそれらをラップするオブジェクトを作ってセットするんじゃ、自分で普通に手 続き的にUI 書くより大変になってるよ!
コントロールもちゃんとデータの内容からだけで毎回生成するように書くのはかったるく て、適当にコード側と癒着させる方がずっと楽だった。
そこで皆割とこれまで通りのGUIプログラムを書いてた。
GUIを準静的に書く、というのは、思想はご立派だが実際にはめんどくさいよねぇ、と いうのが当時の結論と言ってよかろう。
MSから提供されてるデータのクラスやコントロールのクラスを使う時には準静的に書く けど、それ以外のカスタムな所では皆これまで通りのコードを書いていた。
それにしてもなんか標準のコントロールはゴテゴテしてて使いにくいなぁ。
時は流れて。
ゴテゴテしたコントロールは意外と柔軟性があり、結構自分でコードを書かなくてもいろ んな事が出来る事が分かってきた。その範囲では準静的に書く事になるので楽に書ける。
でもその枠から少しはみ出すと全部自分で昔ながらのやり方で書かないといけない。これ は凄いバグるし、ゴテゴテして面倒くさい物を触らないといけない。
もーやだ!何で全部を準静的に書けないんだ!
キレた人々は、これまでの「なるべく多くの物を準静的に書いて残りはこれまで通り書く」 、というスタイルを捨てて、「何がなんでも全てを準静的に書く」という事で揃えるとい う方法を編み出した。
これまでプログラムで実現していた部分を無理やり全てINotifyで通知するプロパ ティを作り、そのやり方では無理そうな事でもとにかくなんとかこじつけてDataBi ndだけで実装する。
例えば、
window.SetCursor(Cursors.WAIT);
model.VeryLongOperatioe ();
window.SetCursor(Cursers.NORMAL);
みたいなのがあったとする。
ちなみに長いオペレーションをUIスレッドでは普通はやらないからそこは本当はasy ncでちゃんと書くが、そこはどうでもいい。
こういう時も無理やり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プログラミングの哲学の歴史
なお、Javaプログラマ向けに一部翻訳してるので、C# の実際とはちょっと違う。
かつて人々は、onclickでリクエストを発行しデータを取ってきて、その間はロー
このシーケンシャルな手続きプログラムは、非同期なGUIという物と大層相性が悪く、す
GUIプログラムのバグはどこから来るのだろうか?
それはページの動的な所から来る、という観察があった。
静的なhtmlはあまりバグらない。
一旦動く、という事が静的に確認されれば、それ以後はバグらない。
でも静的なページだけでは目的の機能は達成出来ない。
そこで静的なページに一部、動的な物を混ぜよう。
でもその混ぜ方を無制限にしてしまうと、JSPのようになってしまって、結局バグが入
そこで限られた形で、動的な部分は最小限にしたい。
そして理想的にはそれは、静的なチェックが通ればバグフリーだと嬉しい。現実的にはそ
そんな「準静的」を目指す。それがAvalonという名のプロジェクトの目指した物だ。
Avalon のチームは、GUIプログラムの動的な要素というのは、パターンがあると思った。
全体としては動的でも、それぞれの状態という概念があって、それが遷移し、それぞれの
その形式に合わない物も多少はあるだろうけど、だいたいは出来る。で、そのだいたいは
さて、状態とは何か?というとオブジェクトで良いだろう。
あるオブジェクトの持つデータの値が決まると、そこから一意にページが決まる。
でもこれだけだとユーザーの操作に反応出来ない。
そこでコマンドと言う物も作られた。
基本的には何かアクションがあると、コマンドという物が実行される。
そして、その結果オブジェクトの値が変わり、その値に応じてまたページが生成される、と
別にコマンドの中で直接UIを変更する事は出来るのだけど、やらない方が良いスタイル
哲学的にはそれで終わりだが、実装的にはUI 側がオブジェクトの変更を知る必要がある。
値の変更をどう伝えるかという事でINotifyPropetyChangedという
OnPropertyChanged(object target, string propName )
を呼ぶ、という紳士協定。
フィールド名の文字列。ださい。型とか無い。
で、XAML側はこのコールバックを設定して、呼ばれたらリフレクションで値とってコ
ICommandは普通のOnClickListenerと本質的には変わらないので
ICommandの先で直接getElementByIdして値変えたりはしない、と
状態を更新してOnPropertyChangedを呼ぶ、というガイドラインになる
つまり通常のGUIプログラミングを
1. コールバックに反応して状態を変更するコード
2. 状態に応じてページを生成する部分
の2つに分ける、というプログラミングスタイルで書きましょう、と言った。
そして2に関しては準静的に書けるようなDataBindという拡張シンタックスがあ
2はうまく作ればほとんどバグらないように作れる。1はこれまで通りバグが入るが、G
これがAvalonだ。
ちょっと準静的、という事について説明を補足しておく。
静的なページを通常の型チェックのあるプログラムに例えると、準静的とはJavaのG
大部分は静的に書いて、そこはチェックされるが、一部Generics引数としてそこ
ただ穴は無制限になんでも入るのでは無くて、ある種の制約がある。
実際のページは状態に応じてその場その場で特殊化される。特殊化された物はほぼ完全な
ただ、ある種の制約はあるので、一つが動けばだいたいは全部動くので、見た目程は違わ
準静的に書ける物としては、基本的には値のマッピングと、対象となるオブジェクトの型
if文とかは書けないので、値をVisibleにマップして擬似if文みたいな事はす
とにかく、宣言的に書けそうな範囲でGenericsが使えるような物と思っておくと、だ
さて、実際にAvalonが世に出てみると、意外とそううまくは行かなかった。
UIの変更の内容が下のオブジェクトとそんなに綺麗にマップされない。
このオブジェクトのこのプロパティとこのオブジェクトのこのプロパティの両方の値を見
コントロールもちゃんとデータの内容からだけで毎回生成するように書くのはかったるく
そこで皆割とこれまで通りのGUIプログラムを書いてた。
GUIを準静的に書く、というのは、思想はご立派だが実際にはめんどくさいよねぇ、と
MSから提供されてるデータのクラスやコントロールのクラスを使う時には準静的に書く
それにしてもなんか標準のコントロールはゴテゴテしてて使いにくいなぁ。
時は流れて。
ゴテゴテしたコントロールは意外と柔軟性があり、結構自分でコードを書かなくてもいろ
でもその枠から少しはみ出すと全部自分で昔ながらのやり方で書かないといけない。これ
もーやだ!何で全部を準静的に書けないんだ!
キレた人々は、これまでの「なるべく多くの物を準静的に書いて残りはこれまで通り書く」
これまでプログラムで実現していた部分を無理やり全てINotifyで通知するプロパ
例えば、
window.SetCursor(Cursors.WAIT);
model.VeryLongOperatioe ();
window.SetCursor(Cursers.NORMAL);
みたいなのがあったとする。
ちなみに長いオペレーションをUIスレッドでは普通はやらないからそこは本当はasy
こういう時も無理やりvmとか言うオブジェクトを作って、
vm.SetCusorValue(Cursors.WAIT);
model.VeryLongOperation();
vm.SetCursorValue(Cursors.NORMAL);
とvmという変数名のオブジェクトのメンバにセットする、という風に無理やりオブジェ
このただUIの状態を変更する為だけに存在するオブジェクトをViewModelと呼
このViewModelの実装で、
void SetCursorValue(Cursor cur) {
m_cursor = cur;
OnPropertyChanged(this, "CusorValue")
}
とかわざわざやる。
元のコードより無茶苦茶長くなっててめんどくさい!
でももう全部こうやって書く、と決めた。
しかもどうせいつかはUIの状態を思い通りにする為に、UIの状態に一対一に対応する
だから最初から全てを諦めて先回りしてそのクラスを作る、と決めた。心を無にして何も
全部UIのプログラムをそのクラスのメンバの値の変更だけで表現する、と勝手に自分ル
これをMV VMパターンと呼ぶ。
VMの定義は本当にかったるくて、普通に書けば簡単に出来る事をわざわざ倍くらいのコ
VMへの翻訳は少しやってみると分かるが単純作業で何も頭を使わない。ひたすら面倒く
さて、ヤケになって、全部を無理やり準静的に書くと、毎回この作業を画面ごとにやる事
何度も同じ単調作業を延々と繰り返すので、すぐに慣れて凄い速度でVMが作れるように
みんな絶対この作業は人間がやるべきじゃない、と思っているのだが、もう慣れてしまっ
結局Windowのメソットを呼ぶのとVMのメソッドを呼ぶのでは、同じじゃん!って
まず、このやり方では書けない、または書きにくいUIが結構ある。
例えばプログレスダイアログを出す、とかやりにくい。メッセージボックス出したりもや
複数のWindowにまたがるのは苦手な事が多い。
つまり、ある種のサブセットに無理やり自分達のUI仕様を押し込めるスタイルとなって
このサブセットで実現出来ないUIを作りたい時はどうするのか?というと、簡単に出来
例えばダイアログの代わりにLoadingと書いてあるラベルをhiddenにしてお
そういう良くあるUIの要求の、VMでの実現方法をみんなで考えて共有してった結果、最
そしてだいたいは、既存の良くあるUIよりは良いUIに出来たので、理論武装も済んだ。何
さて、このVMで表現出来るサブセットに機能を翻訳する、というのがMVVMの根幹だ。
そのサブセットとは、準静的に書けるUIに制限する、という事でもある。
ただしAvalonが出た当初のような、出来る所だけ準静的にする、という姿勢よりは、解
解空間が広がった事と組み合わせの試行錯誤のノウハウが溜まってきた事で、単なるヤケ
そもそもにバグりやすい物とは、準静的に書けない所が多かった。そこを意図的にUIか
さて、これでUIプログラムは全て準静的となり、UIにまつわるバグは激減した。
というよりバグるUIを書くのを辞めた、というべきかもしれない。
バグらないパターンだけでUIを構築するようになった。うまく行く事が分かってる物を
静的なページが一旦表示されればバグらないのに似ていて、準静的なUIも一旦表示出来
その代わりのExcelの経費精算は受け入れる事にした。
これでUIからのバグはほとんど排除出来たのだが、幾つか新たな問題も生まれた。
まずはUIの値の変更の通知とモデルの受け取りたい通知のミスマッチ。
例えばTextがタイプされる都度Suggestを出す、とかの処理をMVVMで実装
入力が終わってFocusOutのタイミングやSubmitボタンが押されたタイミン
また、UIのバグはだいたい駆逐出来たのだが、UIスレッド以外の作業はコンソールア
例えばリクエストを投げて帰ってきたら何かやる、タイムアウトだったら何かやる、みた
長くなり過ぎたのでRxの話はカット(ぉ
追記: 要望が多かったので簡単に続き書きました。 AvalonからMVVM、そしてRxへ(その2): GUIプログラミングの哲学の歴史