この記事では宣言的UIにどのアーキテクチャパターンが良いかを述べるのではなく、宣言的UIでのアーキテクチャパターンの課題とアドバイスを記しています。
概論
私はiOSネイティブアプリのプログラマでした。現在でも個人ではSwiftUIでアプリを作成していますが、業務ではFlutterアプリを作成しています。ですのでAndroidネイティブのことはわからないのでiOSとFlutterを主に話題にします。
iOSのUIKitではMVCが標準のアーキテクチャパターンでした。一方SwiftUIやFlutterは標準のアーキテクチャパターンが存在しません。存在しないとどうなるのでしょうか。去年私が引き継いだFlutterプロジェクトが、存在しない場合の最悪な実装を示してくれました。
簡単に述べると「WidgetがAPIアクセスを行い、JSONから子Widgetが生成され表示する」という実装です。当然Widgetは肥大化してスパゲッティコードになっていました。
UIKitではMVCのCが肥大化する問題が注目されていますが、標準アーキテクチャパターンが存在することで少なくともVとCは分離され、Vの見通しはある程度良い状態が保たれていました。
React Nativeは詳しくないのですがReactと同様にReduxが一般的のようで、Reduxに乗ってしまえば設計に関して問題は発生しにくいと言えます。
しかしSwiftUIやFlutterでは上記のようなVにすべてを載せてしまう実装まですでに発生しているのです。この2つのフレームワークではアーキテクチャパターンによる設計を十分検討して適用する必要があると言えます。
MV
SwiftUIやFlutterではMVで良い、という意見をよく目にします。
- VがMを購読
- Vがビジネスロジックの起動を依頼
という構造です。この構造、どこかで見たと思いませんか?そう、MVCです。AppleのMVCはCがMとVの仲立ちをするのですが、非AppleのMVCでは上記のような構造になっています。「宣言的UIではMVで良い」という意見は結局とのところMV(C≒M)だと言えます。
ここでの課題はMのVへの反映とC≒Mの設計です。
Vへの反映でどれだけプレゼンテーションロジックが存在するかによりVの複雑性が増大するかが決まります。Mの持つデータをそのままVで利用するだけなら問題は発生しませんが、変換や分岐が多くなるほどVが肥大化します。もし肥大化する場合はMを変換してVで利用しやすい形に変換することになり、それはVMからコマンドを無くしただけの存在が生まれます。この存在をプロジェクト内でどのように位置づけるか、どのように生成するかを明確にしておくと良いです。
C≒Mの設計はVが複数のモデルを操作するなどユースケース的な記述をどこにするのかが課題になります。Vにユースケースを記述するとVが肥大化してアンコントローラブルになっていきます。ユースケースを別に記述してそれを起動する形にするなどC≒MではなくCを単独に作成することを意識すると良いと考えられます。
Mの設計に関してはDDDが役立ちます。ただ、DDDは一般的にアプリケーションドメインの知識を実装するので、アプリケーションドメインを抽象化してプログラミング可能にしたソリューションドメインの設計まで持ち込めていません。ですので他の抽象化にまで踏み込んだ設計に関する書籍も参照すると良いと思います。マルチパラダイムデザインやLean Architecture: for Agile Software Developmentをおすすめしておきます。
MVVM
「宣言的UIはバインディングがあるのでMVVMは不要」という意見がありますが、MVVMはバインディングにより成立するアーキテクチャパターンなので本来はiOSのRxSwiftなどのFRPライブラリを利用したMVVMが邪道であり、バインディングがあるからこそMVVMが可能と言えます。
(UIKitでMVVMを採用するとVMの実装が冗長になり、MVCの方がシンプルで良いのではと思います。2013年にObjCのReactiveCocoaでMVVMを実現する記事を書いた者としては紹介すべきでなかったのかもという思いがあります。)
Flutterにはバインディング機構が存在しないので本来的なMVVMは不可能です。ただしRiverpodを利用することでMVVMに近いことは可能です。
MVVMは不要という意見はVがMを直接扱えばVMのコードがなくなって良い、ということでしょう。確かにMがVにそのまま利用できれば、です。
VMはVから状態とロジックを排除します。MをVに反映するのが複雑になるほどVが複雑になり宣言的UIの利点である高速なUIの構築・変更が行いにくくなります。
私は現在、業務でプロトタイプアプリを作成しているのですが、MをVが直接参照することでVに状態やロジックが含まれ、次第に高速な変更がしにくくなりました。そこにVMを導入することでまたVの書き換えが高速に行えるようになりました。
プロトタイプでなくてもVMがあることでモデルの変更がVに波及しにくくなります。
実はVMは薄い層だとされています。過去に関わってきたプロジェクトではVMが肥大化していることがよくありました。単純にVの状態とプレゼンテーションロジック、そしてモデルのロジックを起動するだけのコマンドを持つだけのVMであれば軽量なので実装の邪魔にはならず、逆に実装の高速化に寄与します。
そのためにはMVで述べたようにMの設計を行えるようになる必要があります。
関数型アーキテクチャパターン
Elm Architectureを嚆矢とするSwiftUIのTCAなど、関数型アーキテクチャパターンが注目されています。
データが1方向に流れ、副作用が分離されるのでバグが少ないという利点があります。
利点に関してはよく述べられているのでここでは課題を挙げてみます。
そもそも関数型アーキテクチャパターンは関数型プログラミングを用います。ですので関数型プログラミングに精通する必要があります。また、関数型プログラミングでのモデリングが必要になります。関数型モデリングはまだあまり普及していないように思います。もしオブジェクト指向などが入り込むとせっかくの副作用の分離が壊れてしまう可能性があります。
よって関数型アーキテクチャパターンを採用する場合にはプロジェクトメンバーが
- 関数型プログラミング
- 関数型モデリング
に精通する必要があります。これは結構ハードルが高いと思われます。TCAは良いと聞いて採用する動きがありますが、この点を踏まえて採用を検討するとよいでしょう。
そのためにはまず学習です。
関数型プログラミングの学習には、入門としてプログラミングの基礎がおすすめです。学習が進むと関数型プログラミングの基礎を読んでみるといいでしょう。Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイドは良書ですが難しい書籍です。ある程度学習が進んでから手に取ると良いです。
関数型モデリングの学習はDomain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#をおすすめします。関数型プログラミングでDDDを行うという内容です。
学習するためのプログラミング言語として「プログラミングの基礎」がOCamlを採用しています。「Domain Modeling Made Functional」が採用しているF#はOCamlの影響を受けて作られた言語なので「プログラミングの基礎」のあとに「Domain Modeling Made Functional」を読むとわかりやすいと思います。
まとめ
FlutterやSwiftUIはデフォルトのアーキテクチャパターンが存在しないので、アーキテクチャパターンの選定が重要です。
宣言的UIの利点である高速なUIの作成を邪魔しない設計が求められます。
MVやMVVM、関数型アーキテクチャパターン、それ以外のアーキテクチャパターンでも最終的にはモデルの設計が重要になります。
良いアプリを作っていきましょう。
モバイルアプリのプログラマや技術顧問としてフリーランスで活動しています。お仕事の相談や依頼はTwitterでDMを送ってください。最近は美大出身でプロダクトデザインなどを行ってきた妻が一緒に仕事をするようになりました。プロダクトの要件・仕様の策定のお手伝いやプロトタイプの作成も可能です。ご連絡お待ちしております。