なぜiOSのMVVMはdisられるのか — Elm Architectureとの比較記事から考える
iOSアプリではMVVMが多用されている。UIKitとFRPライブラリであるRxSwiftを組み合わせて実装されるのが一般的である。(私はReactiveSwiftの方が好きだけど…)
MVVMはマイクロソフトのWPFで考案されたソフトウェアアーキテクチャパターンで、それがiOSに導入されて広まった。
しかししばしばiOSにおけるMVVMは批判の的となってきた。もっとも俎上に上がるのはVMの肥大化・複雑化である。最近では以下の記事があげられる。
なぜ MVVM は Elm Architecture に勝てないのか
この記事を元になぜMVVMが批判されるのかを見ていこうと思う。
ViewModelは複雑化する論
上記記事ではやはり「複雑化しすぎたViewModel」と主張している。
複雑化したコードが掲載されているので全文は転載しないが、主要な個所を見てみる。
まずViewModelの状態の管理を検討する。
private let state: BehaviorRelay<String> = .init(value: “!!!”)…public func doSomething() -> Observable<String> {…
このコードは一般的にコントローラーにおいてViewとバインドされる時に利用され、バインドのためにRxSwiftの部品を利用している。stateプロパティに格納してそれをdoSomethingで出力している。ここでWPFのコードを参考(掲載しないのでコードを検索してね)にしてみると、ViewModelのプロパティは特段FRPライブラリを利用しておらずget/setを定義したプロパティである。そしてWPFには独自のバインディング機構を利用してViewとなるXAMLにおいてViewModelのプロパティを参照してViewの中でバインドを行っているのである。
iOSではFRPをバインドに利用することが多いが、そのために2重のコードになり肥大化が起こっている。
またViewModelの肥大化と関係するが、Controllerでのバインドの記述が長くなるのもバインディング機構が無いためである。Controllerでバインドを行うのはAppleのMVCに見られるViewとModelの間にControllerが介在する実装と双子のようである。
これはViewModelとViewを疎結合にするために行っているように感じられるが、WPFを参考に実装するならばViewModelをViewに渡してBehaviorRelayを利用しないプロパティをViewで参照する、もしくはなんらかのバインディング機構を用意すると良いのである。私はそこまでMVVMの研究を行っていないので、実際にどのように実装すれば良いかは提示できない。だがiOSにおいてよく行われているViewModelの状態の管理は参考としたWPFの手法とは似ても似つかないものになっているのである。今回はUIKitでの実装を検討しているが、これがSwiftUI+Combineでの実装となると話が違ってくる。@Publishedを付与したプロパティをViewでバインドできるようになるのである。
ちなみにElmアーキテクチャをUIKitで利用するのもまた別の問題が発生する。これはUIKitと組み合わせるために発生するものであるといえる。
まとめると、UIKit+RxSwiftで一般的に行われているバインディングは間違った(とまでいっていいのか分からないが…)場所で、行いにくい方法で行っており、上記記事もそれを踏襲しているのである。
次に入力から出力までの流れを検討する。
ここでまず以下の行に連なるコードに注目してみる。
output1 = input1.asObservable()
これはinput1からの入力をViewModelの状態と合わせてoutput1に出力している。ところで一般的なMVVMの入力はモデルレイヤーのオブジェクトに渡されて処理され、その結果であるModelの状態をViewModelが受け取って表示に適した状態に加工する。しかし上記のコードは入力をViewModelで処理し、ViewModelの状態を利用して新しい状態を作成している。これはModelでの処理を、間違ってViewModelで行っているのである。この間違いは結構多いらしく、ViewModelの肥大化・複雑化を招く。
また
output2 = input2.asObservable()
上記に連なるコードではinput2の入力に多くのViewModelの状態を利用して出力としている。ViewModelは入力の加工に利用するのは間違いでModelに入力を渡すのが由緒正しい実装であろう。
さらに
output3 = Observable.combineLatest(input3, input4, output2)
以下のコードでは複数の入力とViewModelの状態を組み合わせている。入力はモデルに渡してそこで合わせるなり何なりをするのが由緒以下略。
つまり参照記事のコードは意図的か分からないが悪い見本を提示しており、悪い見本をリアクティブ・スパゲティと読んでいるのである。確かによく行われる間違いであるが、これを元にiOSでのMVVMを批判するのは不公平の感が否めない。
iOSのMVVMがdisられるのは、このようなことから起こっているのではないかと考えられる。
またiOSにおけるMVVMはFRPライブラリを利用せずとも実装できるのである。つまり参照記事がFRPを前提とした比較を行っているが、それはFRPを利用することが多いだけで、FRPを利用しない別の比較もできるのである。当のElmがFRPとさよならしたのであるし。
MVVMはどのような課題を解決しようとしたのか
そもそもMVVMはどのような課題に対応しようとしたのであろうか。一つにはViewにおける状態と状態を作りだす処理をViewから引きはがすことであるといえるのではないだろうか。MVCはModelを独立させModelの状態を用いてViewを更新する仕組みである。理想的には状態は全てModelにあり、Viewはそれを表示するだけである。しかし現実的にはModelからViewに適した状態を作りだしその新しい状態を管理する必要がある。これを私は勝手にViewへのラストワンマイル問題と読んでいる(オレオレ用語である)のであるが、Viewへ最後に、どこで荷物を積みなおし、管理し、運ぶのか、である。その役割がMVCではViewに存在し、MVVMはそれらをViewModelに移管したのである。これによりViewは軽量になり描画だけの役割となったことで宣言的な実装が可能になった。
では件の記事で比較されているElmアーキテクチャはどのような状況であろうか。本家Elmではコンポーネント論争が行われていたらしい。要はUIの状態をどこで管理するかである。ElmのViewは純粋関数で状態は持てない。であるならModelがUIの状態を管理するのかというとそれはそれで避けたい。であるならコンポーネントを作るのか。という話である。結局のところコンポーネントは不適とされたらしいが、現在どのような実装か詳しくは知らないがElmにおいてもViewの状態の管理は課題となっていた。また一般的にUIの状態はViewの関数のなかでModelから生成されて利用される。つまりMVVMが解決しようとしたラストワンマイル問題は残されているのである。Elmアーキテクチャでの状態の管理に関してElm以外での実装でメモ化を利用したものを見たことがある。View関数の中で管理する手法の一つであるといえる。
これはElmアーキテクチャだけではない。ReduxにおいてもViewの状態の管理は課題であり、Reduxで管理する状態にModelとViewの2つの状態を入れ「正規化」することで管理し、Viewの状態を生成するのもreducerもしくはViewで行う必要がある。Redux+MVVMでその課題を解決しようとした例もあるのである。
The Composable Architectureはlocal storeを利用することでViewの状態を別個に管理することは可能らしい。ただlocal storeはModelの状態をlocal stateに変換するために存在するので、reducerの役割にViewの状態へ変換する機能が含まれることになる。レイヤーの概念を持ち込むと同じレイヤーで処理していることになる。
つまり関数型アーキテクチャパターンはMVVMが解決した課題をいまだ持ち続けているのである。
これらの関数型アーキテクチャパターンは、SwiftUIのViewが状態を管理できるので、SwiftUIの力を借りて活用することができる。Viewのキャッシュもあるらしい。作成した状態を補完しておく必要も少ないのかもしれない。また、CombineというFRPライブラリの力を使うことで実装が楽になるだろう。これはiOSにおけるMVVMにも恩恵をもたらす。まぁ富豪的ではあるが。
またアーキテクチャパターンではないがReact HooksやRecoilは状態の管理に注目しているようである。iOSにおいてもそれらを参考にしたライブラリが発生して関数型アーキテクチャパターンと組み合わせる流れが発生するかもしれない。
おっとClean Architectureを忘れては困る
みんな大好きClean Architectureである。VIPERも入れようか。Clean Architectureには基本的にViewModelは含まれないと考えられている(よね?)。Clean ArchitecruteはPresenterでViewを抽象化する。がViewModel程の抽象化はできず、バインディングも一般的には行わないので、SwiftUI時代にはそぐわない…わけではなくClean Architecture自体が自由に変更可能であるからViewModelを組み合わせてもClean Architectureだと言い張ることは可能ではないかな。ずるいけど。
MVVMはElmアーキテクチャに勝っているのかいないのか
MVVMはモデルの詳細な規定が無い。モデルは自由である。からしてモデルの実装が難しくなる。これがViewModel肥大化の要因の1つである。しかしこの点で「勝っていない」といいたいわけではない。MVVMのモデルが規定されないことでReduxと組みああせても(Clean Architectureと組み合わせても?)多くの人の認識はMVVMの様である。これは長所であるとも言える。しかしこれでも勝っているとは言えない。全てのアーキテクチャパターンはそれぞれ解決しようとする課題が別であるから別の実装であるといえる。そしてソフトウェアの課題はそのビジネス要件やフレームワーク要件により多岐にわたり特定のアーキテクチャパターンはあるの課題には対応できるが、別の課題に対応できない場合がままある。であるからアーキテクチャパターンから要素を取り除いたり組み合わせたりして対応する。ElmアーキテクチャやReduxは要素を取り除くことは難しいが、MVVMと組み合わせた事例のように、相互で補い合うのである。MVVMが必要ないなら使わなくて良いし、必要であれば使えばいい。よって勝っている・負けているということは存在せず、それぞれが今後も発展しつつ補いつつ存在していくと私は考えている。
基本的な実装をした方がチームメンバーの認識の統一が測りやすいって?そういう場合はArchitecture Decision Records。
はいっ!次回「Architecture Decision Recordsを開発に組み込もう!」乞うご期待!
追記
参照記事で、FRPを利用した入出力が似た形になるのは当然であると考えられる。しかし、数学的に一方が一方を構成できると言っても、その入出力がモデルの状態に関する入出力なのか、ビューの状態の入出力なのかで役割が違い、代用できるかは役割による。FRPを基礎としないMVVMとElmの比較をFRPを通して行なっており、恣意的な感が否めない。著者は数学に基づく関数型プログラミング理論の研究をされ、SwiftでFRPを利用したElmアーキテクチャライブラリを作成されているので、それが話の発端であると考えられる。
ただ、私は上記の分析にはそれほど興味はなく、なぜdisられることがあるのか、どうすればdisられずに活用していくことができるのかに興味があるのでこの記事を書いた。そのような視点で読んでいただくと光栄である。