滅入るんるん

何か書きます

MVVMとはなんぞやを公理から求めてみる

MVVM(Model-View-ViewModel)はC#/.NET*1の世界で生まれたアーキテクチャーですが、今では他の世界(Androidとか)でも利用されています。

しかしながら、C#/.NETの世界から他の世界へ輸出される際に間違った解釈で移されていたり、言語・フレームワーク上の性質から妥協をしすぎて本来のMVVMとは言えないものまでMVVMと呼ばれていることもあります。

今回はそのMVVMの公理を定めて、「公理」から「定理」を導いてみようというものです。

アーキテクチャーを語るうえで重要なこと

アーキテクチャー(および設計)は多種多様な価値観があり、それぞれの側面から見たら正解であり違う側面から見たら間違いであることがあります。

最初の考案者が提案したアーキテクチャーが進化と伝染を遂げるごとに最初に提示されたものとは違うものがそのアーキテクチャーとして認識されることもあるでしょう。人々が認識しているそのアーキテクチャーは時代とともに変わっていく可能性があるのです。

また、アーキテクチャーには最適解という答えは無いようにも感じます。現実的には不可能だからなどの理由で妥協を重ねることもあるでしょう。

今回提示する記事も原理主義的な考えをすれば正しいかもしれないですが、どこを妥協するかによっては間違っているということにもなりかねません。
しかしながら、妥協するということはどこかで妥協した代償が付いてきます。そもそも代償がなければ妥協という言い方自体がおかしなものになりますね。どこを妥協したらどのような代償が付いてくるかは「定理」が導かれた流れを知っていれば多少は想像つくかもしれません。

そもそも「公理」とか「定理」について

大学数学は少ししかかじってないので正確な解釈ができているかはわかりませんが、「公理」というのは物事を考える上での前提のようなものです。

たとえば1 + 1 = 2というのは一般社会からすればごく普通の等式ですが、これを証明するというのは難しいとも言われます。(たしか、1という数値の次に2が来るとか、n + 1を計算するとnの次の数値になるとかそのあたりの話から証明が必要だった記憶)
この「公理」という名の前提をどう定義するかで証明というのは変わってきますが、だいたいの場合は誰もが認めるだろうことわりになるかと思います。

次に「定理」ですが、こちらは「公理」は正しいものだとして導かれたことわりのことです。逆を言えば「定理」が間違ってる場合は導きが間違っていたということになります。

今回は誰もが認めることから誰もが認めざるをえないことを導くために「公理」や「定理」といった考えを使っていくことにします。

MVVMの「公理」

  1. View, ViewModel, Model層が存在する
  2. ViewModel層はView層とModel層のつなぎ目
  3. 各層は疎結合を目指す

今回はMVVMにおいての「公理」を上記のものとして定めます。

1.と2.については名前からわかりきったことなので特に異論は出ないでしょう。3.については「公理」としては結構とびぬけたものとなっているので「公理」として認めれない人もいるかもしれません。今回3.を定める理由としてはこれを前提に考えなければ話が進まないからです。アーキテクチャーとして確立するには各層が役割を分担する必要があります。役割を分担するとおのずと疎結合に近いものになっていき、よい設計とはなにかを考える時それは密結合ではなく疎結合という""風潮""があるからです。

本来ならば3.については別途証明*2のようなものが必要かもしれませんが、そんなことまでするのはめんどくさいで今回は直球に「公理」として定めました。
そのため、3.について誤りであるならばこれから話す「定理」はほぼすべてが誤りである可能性があります。

MVVMの「定理」

定理1. View層はViewModel層のことを知っていてViewModel層はModel層のことを知っているがその逆はない

まず最初に、公理1.と2.より各層はView <-> ViewModel <-> Modelといったように接し合っていることになります。

公理3.より各層は疎結合を目指すことになります。ここでの導入では各層が完全に疎結合(つまりお互いにお互いのことを知らない)という状態から考えていきます。

少しプログラムを書いたことがある人ならわかるかと思いますが、このような状態は現実的に不可能です。プログラムにはエントリーポイントというものがあり、そこからプログラムコードを読み取り実行していきます。MVVMが採用されるだろうGUIアプリケーションではエントリーポイントはほとんどの場合でView層*3となっています。そこから各層のプログラムコードを読み取っていくことになるのですから、View層はViewModel層のことを知る必要が生まれ、ViewModel層はModel層のことを知る必要が生まれます。

この考えは構造上の妥協のようなものですが、現実的にはこうするほかありません。しかしながらView層がViewModel層のことを知るのだからViewModel層はView層のことを知っていいということにはなりません。公理3.より各層は疎結合を目指すことになっているので、結合度が上がる行いは最小限でなければなりません。

まとめ

  • ViewはViewModelのことを知っている
  • ViewModelはModelのことを知っている
  • ViewModelはViewのことを知らない
  • ModelはViewModelのことを知らない

定理2. View層はModel層のことを知らない

定理1.の続きになります。

定理1.ではView-ViewModel-Model層の隣り合った関係が導かれました。しかし、それでは一点疑問が生まれます。
ViewはViewModelのことを知っているし、ViewModelはModelのことを知っているのだから、ViewはModelのことを知ってもいいんじゃないのかということです。その答えはNoで、理由は単純にViewの越権行為ということです。アーキテクチャーとして重要になるのは責務の分担であります。Modelのことを知っているのはViewModelだけで十分という考え方で、ViewがModelのことを知ってしまうとView-Model間の結合度が上がってしまいますがそれは公理3.に反したことなので最小限でなければなりません。ViewがModelのことを知りたくなることがあるのならばViewModelを経由すればいいのであるからこの疑問の答えはNoになります。

まとめ

  • ViewはModelのことを知らない

定理3. 下位層は上位層に状態とその変更通知と処理を起動するための戻り値なしのトリガーのみを公開する

定理1.と2.より各層はレイヤーのように上位層・下位層といった関係になることが導かれました。

公理3.の各層は疎結合を目指す上で上位層が知らなければならない下位層の情報として、下位層の状態とその状態の変更通知が必要であるということはすぐにわかるかと思います。

しかしながらそれだけでは不十分で、上位層は下位層の処理を起動する権限(トリガー)を持たなければなりません。
そのトリガーというのはViewModel層ではコマンド、Model層では関数と呼ばれます。(正確にはViewModel層のコマンドはC#/.NETで言うところのICommandインターフェースのようなものでも関数でもよく、Model層の関数は言語によってはメソッドとも呼ばれる)
このトリガーというものが厄介で、上位層としては処理を起動するだけ十分でありますが、関数なので戻り値を返すことができます。しかし、公理3.によって疎結合を目指さなければならないので、上位層は下位層の関数の戻り値に依存してはいけないのです。

この流れをまとめると、上位層は下位層の処理を起動するだけでその戻り値は受け取らず、処理の結果は下位層の状態と状態の変更通知によって検知するということになります。

ちなみにこの話は↓の記事が参考になりますし、私自身かなり影響を受けています。

ugaya40.hateblo.jp

まとめ

  • ViewModelはViewへ状態とその変更通知と戻り値のないコマンドを公開する
  • ModelはViewModelへ状態とその変更通知と戻り値のない関数を公開する

定理4. ViewModelはModelの情報をViewが扱えるように加工をするだけ

定理2.よりViewはModelのことを知らないので、Viewは基本データ型やプリミティブ型などの基本的な型しか扱えません。そこで、Modelの処理の結果をどこかでViewのためにViewが扱えるデータ形式に変換する必要があります。
どこでViewが扱えるデータ形式に変換するかを考えたとき、それはViewModelしか残っていません。

また、GUIアプリケーションを構成するうえでプログラムの役割を大別するとUI描写とロジックになります。UI描写はView層が行うので、残りのロジックを担当するのはどこかということになります。
よい設計とはなんなのかを考えたときそれは責務の分担ができたものであります。また、同じ役割を複数の層で担当してしまうとそれらの層の間での結合度が上がったり、この処理どこの層で書いたらいい?という疑問が頻発することになるので、1つの層で役割を完結させる必要が生まれます。
ロジックについては下位レイヤーのことなので、最下層のModel層が担当することになります。

これらのことからViewModelの役割はModelの情報をViewが扱えるように加工するだけでよいということがわかるかと思います。

また、同様な役割がViewModelに存在するということがIntroduction to Model/View/ViewModel pattern for building WPF apps – Tales from the Smart Clientにも書かれています。(英文なので正確には読めてないですが…)

The ViewModel is responsible for these tasks. The term means "Model of a View", and can be thought of as abstraction of the view, but it also provides a specialization of the Model that the View can use for data-binding. In this latter role the ViewModel contains data-transformers that convert Model types into View types, and it contains Commands the View can use to interact with the Model.

また、定理2.よりViewがModelのことを知りたければViewModelを経由することになっています。経由するというのはModelのトリガーを起動するだけのトリガーをViewModelが公開したり、Modelの状態やその変更通知から得る情報をViewが扱える情報として加工しその情報の状態と状態の変更通知を公開するということです。

まとめ

  • ViewModelはModelの情報をViewが扱えるように加工するだけ
    • ViewModelがModelの状態とその変更通知から得る情報をViewが扱えるように加工し、その情報をViewへ状態と状態の変更通知として公開する
  • ViewModelのコマンドはModelの関数を起動するものとして公開することもある
  • ロジックはModelに書く

Tips

MVVMがクロスプラットフォーム開発に使えるアーキテクチャーというのは副次的効果

ここまでMVVMの「公理」と「定理」を述べてきましたが、その中にクロスプラットフォーム開発という単語やそれを連想させる単語はありませんでした。
これはクロスプラットフォーム開発のためのアーキテクチャーとしてMVVMがあるのではないということです。しかし、クロスプラットフォーム開発のためにMVVMを採用するということもできます。

どういうことかというと、クロスプラットフォーム開発するうえでプラットフォーム固有な機能というのは厄介なものです。それをできるだけ取り除くかひとまとめにするというのがクロスプラットフォーム開発では求められます。
GUIアプリケーションにおいてプラットフォーム固有な機能として真っ先に上がるのはUIでしょう。UIはMVVMにおいてはView層が担当しています。もちろんUI以外のプラットフォーム固有な機能も存在します。たとえばGPSとかネットワークを扱うロジック部分。逆に考えてみればこれらのロジック部分さえView層に持っていったらViewModel以下の層はクロスプラットフォームなレイヤーになることができます。
そのため、MVVMでクロスプラットフォーム開発を行う際はよくプラットフォーム固有な部分をServiceとしてView層からModelのほうへ注入したりします。

また、責務の分担という観点からすればプラットフォーム固有な機能はService層のようにレイヤー分けしておいたほうがいいかもしれません。
MVVMがクロスプラットフォーム開発に使えるというのは副次的な効果と言いましたが、責務の分担をやっていくと自然とクロスプラットフォーム開発に使えるようなものになったりしますし*4、クロスプラットフォーム開発に使える設計にしておくのってロマンありますよね。*5

DataBindingは必要なのか

C#/.NETの世界でMVVMが誕生した際にはXAMLによるDataBindingをすると楽だよ、Viewが抽象化できるよとかそういうのでXAMLが必要といった風潮*6があるかと思います。
しかし今回定めた「公理」や導かれた「定理」からはDataBindingが必要というところまで導けていません。これは(少なくとも今回の話の中では)必ずしも必要と言うことではないということです。

楽だからDataBindingを使うのであって、DataBindingがない環境ではコード上でViewとViewModelを紐づければいいのです。

責務の分担を頑張れば各層ごとに作業の分担ができる

※実際にチーム開発でこの話のことまでできてないので(私の中では)理論上の話です

View, ViewModel, Model層の疎結合や責務の分担を目指した内容になりましたが、これらのことを行うと各層ごとに人員を配置することができます。密結合に近い設計であればあるほど画面ごとに人員を配置して作業の分担をすることになりますが、Viewが得意な人もいればModelが得意な人もいることでしょう。それらの人が得意分野で作業し生産性を向上させるためにも各層ごとに人員を配置するということは有益なことです。

MVVMでも今回導いた「定理」を守れば、レイヤーごとの人員配置は可能なはずです。ただ、そのためには頑張らないといけないことがあります。
ViewはViewModelのことを知っていてViewを作り上げるのだからViewModelが公開する情報があらかじめ決まっていないとViewは作り上げることはできません。またそれはViewModelがModelについても言えることですが、作業の進行速度を考えるとViewModelを担当する人は最初にViewへ公開する情報のインターフェースを決める作業をすることになるかと思います。

たとえばですが、Viewを担当する人は初めはViewModelに依存しない作業をし徐々に依存することの作業をする、Modelを担当する人は単純にModelの作業をする、ViewModelを担当する人はViewに公開する情報のインターフェースを決めてから、決めてる間に作業が完了したModelに対応するViewModelの作業をする、といった感じです。

ただし、これは私の頭の中の理論上の話です。実際にはもう少しクリティカルパスの解消の必要性があるかと思いますが、MVVMをガチですると各層ごとに作業の分担ができるはずです。*7*8

Model層の非同期メソッドの戻り値voidにするかTaskにするのか問題(C#/.NET)

C#やそのほかの.NET言語では非同期メソッドがあります。これは非同期処理する上で非常に優れていて、モダンな開発をするならば、Model層ではasync/awaitを使うことになるでしょう。

しかし、非同期メソッドでは厄介な問題があり、戻り値をvoidにするかTaskにするかで迷うということです。 まず非同期メソッドには以下の戻り値を設定できます。

  • void: 戻り値なし
  • Task: 待機可能であり戻り値なしを表す
  • Task<T>: 待機可能であり戻り値がT型であることを表す

※正確にはC# 7.0よりTask-likeな型であれば非同期メソッドの戻り値に設定できるようになりましたが、説明の簡素化のためここではTaskと表記しています。

定理3.よりModelがViewModel公開するメソッドは戻り値なしであることが導かれました。非同期メソッドではvoidまたはTaskということになりますね。
それら2つには問題があります。voidを戻り値にした非同期メソッドでは、非同期メソッド内で発生した例外が虚空の彼方へ消えていく可能性があります。Taskを戻り値にした非同期メソッドは発生しうる例外がViewModelへ伝搬し、待機可能性をViewModelへ持たせてしまいます。

これをどうするかは、どう妥協するかということになります。また、同じような議論は以前にもあったようです↓

togetter.com

参考までに、個人的な現在の考え(妥協ポイントなので変わるかもしれない)はasync voidでtry-catch-Exception派です。

public async void Method()
{
    try
    {
        // 処理
    }
    catch(Exception e)
    {
        // 状態をエラーに変更
    }
}

まとめ

  • View
    • ViewModelのことは知っているがModelのことは知らない
  • ViewModel
    • Modelのことは知っているがViewのことは知らない
    • Viewへ状態とその変更通知と戻り値のないコマンドを公開する
    • ViewModelはModelの情報をViewが扱えるように加工するだけ
    • ViewModelのコマンドはModelの関数を起動するものとして公開することもある
  • Model
    • ViewModelとViewのことは知らない
    • ViewModelへ状態とその変更通知と戻り値のない関数を公開する
    • ロジックはModelに書く

最初のほうに述べましたが、今回定めた「公理」が間違っていたらこれらのほとんどは間違っていることになるかもしれませんし、「公理」からの導きが間違っていたら間違った「定理」に行きついてる可能性もあります。

*1:C#/.NETと表記してるのは単純に.NET言語の中で自分がC#を推しているということであり、だいたいのことは.NETという表記でいいと思います

*2:責務の分担とか疎結合のほうがいいとかはもう少し深く掘り進んだほうがいいかもしれない

*3:複数のエントリーポイントのあるGUIアプリケーションとか考えると面白いかも

*4:このあたりからClean Architectureな考えに突入していく……

*5:MVVMでクロスプラットフォーム開発はロマン

*6:私はAndroidとかXamarin.Androidの世界の人間なのでXAMLのことはあまり詳しくない……

*7:実際にこういった開発をやってみたい

*8:別にMVVMに限った話じゃなくて他のアーキテクチャーでも頑張ればできると思います