iOS関係の勉強会に参加するとほぼ間違いなく、設計に関する発表があるように思います。
「RxSwiftを使ってMVVM...」「Clean Architectureを導入...」, etc...
色々話を聞く中で、自分は以下のような課題があるなぁと感じています。
- いろいろな設計方法があるけれど、結局何を使うべきなのかわからない
- 名前は聞いたことがあるけれど、それぞれがどのような設計で、何がメリットなのかわからない
- 勉強した時は分かったような気がしたけれど、もう忘れた
この記事はこれらの解決の一助になればと思って書いたものになります。(設計へのモチベーションを上げたい)
サンプルコードを交えながら、5つの設計について考察してみます。
※ RxSwiftの名前を出しましたが、ライブラリに関してはこの記事では言及しません。
そもそも、なぜ設計に拘る必要があるのか
iOSアプリ開発において、このような課題があり、それを設計によって解決しようとしている流れがあると思っています。
- ViewControllerの肥大化 (Fat View Controller, Massive View Controller)
- 1000行超えのVCがつらい
- ロジックの複雑化
- アプリのアップデートごとに機能を付け足してたら、状態管理がめちゃくちゃになった
- テストができない
- 単体テスト・UIテストを実装したいけど、各モジュールの依存関係が強すぎて\(^o^)/
- 属人化
- 開発チームメンバーが入れ替わることになった際の引き継ぎが難しい
- どのクラスがどの役割を持つのかが人によって違い、可読性が低下している
他にもまだまだあると思います。
設計を明確化するメリット
挙げたような問題は実装方法に自由度がありすぎてしまうことが大きな要因です。
決められた設計に準拠し、あえて制約を作ることで、実装に統一感を出したり、本質とは異なる余計な思考 (依存性注入、単一責任の原則などなど) を減らしたりすることができます。
独自にモジュールの役割を決めることもできますが、名前の付いている設計(=共通認識がある設計)を導入することで、新メンバーが加入した際もスムーズに開発をすすめることができます。
設計を明確化するデメリット
慣れるまで、開発速度が落ちると思います。(学習コスト)
また、設計のパラダイムシフトが起きた時に移行が難しくなるかもしれません。
サンプルコード
前置きが長くなりすぎましたが、本題に入りたいと思います。
エンジニアは自然言語よりも形式言語の方が理解しやすいと思うので、サンプルコードを書きました。
設計ごとにTargetを分けてみました。
実行するとWebAPIから画像を取得し、猫のgif画像が表示されます。下に引っ張って更新ができるアプリです。
(Carthageを使用しているので、実行前にcarthage bootstrap --platform iOS
をする必要があります。)
以下を読む時はこのサンプルコードも参照しながら見ることをオススメします。
設計一覧
- MV□
- MVC
- MVP
- MVVM
- VIPER
- Clean Architecture
MV□
- MVC
- MVP
- MVVM
役割
Model
データを保持する役割。
場合により、データのフェッチや、ビジネスロジックも役割に含まれる。
View
GUI。UIKitを扱う役割。
MVC
Model View Controller
サンプルコード
https://github.com/tattn/ios-architectures/tree/master/ios-architectures/MVC
iOSでは一般的な設計。
https://developer.apple.com/jp/documentation/CocoaEncyclopedia.pdf
図を見ても分かる通り、iOSのMVCはControllerが肥大化するため、
Massive View Controllerの略だとよく揶揄されます
Massiveになる原因はライフサイクルや様々なdelegate、ビジネスロジックがViewControllerに集中するためです。
それを回避するために、UITableViewDataSource
やビジネスロジックをModelに移し替えて、軽くするということがよく行われています。
そのため、Model
という言葉でも、人やプロジェクトによって解釈が大きく異なる可能性があります。
(MはSwiftyJSON
に任せるだけなんてことも)
サンプルコードは全部ViewControllerに載せるよくあるパターンになっています。
(本来のMVCについてはwikiが参考になります)
評価
- わかりやすさ (使いやすさ)
- Cocoaのフレームワークを素直に使え、馴染みやすい
- テストのしやすさ
- VとCが密結合になるため、Mのみのテストになりがち
- 実装のしやすさ
- UIKitを素直に使うだけなので実装しやすい
MVP
Model View Presenter
サンプルコード
https://github.com/tattn/ios-architectures/tree/master/ios-architectures/MVP
図ではMVCとあまり変わりませんが、実装上はMVCのクラスに加えて、Presenterが増える形になります。
ViewControllerはViewに含まれます。
Presenterの役割
Viewで表示するために必要なデータをModelから受取り、Viewに渡す仲介役。
ライフサイクルやレイアウトに関する実装は含まず、UIKitには依存しません。
(サンプルコードではUIKitをimportしているのはViewだけになっています)
特徴
ViewControllerをPresenterとして分割することができるので、肥大化しにくくなっています。
UIKitの依存関係も明確になっています。
Viewはモデルを直接参照しません。
そのため、Presenterをモック用に差し替えることで、Viewのテストが可能になります。
個人的評価
- わかりやすさ (使いやすさ)
- シンプルな設計で、役割もわかりやすい
- UIKitに依存している部分とそうでない部分で切り分けられるので、ViewControllerは肥大化しにくい
- テストのしやすさ
- MはもちろんPやVもモックを作りやすく、テストしやすい
- 実装のしやすさ
- MVCと比べるとコード量がPとそのインターフェース分、増える
残念な点
ViewControllerは軽くなりますが、図の通りMVCと全体的には変わらないので、モデルの曖昧性などは変わりません。
MVVM
Model View ViewModel
サンプルコード
https://github.com/tattn/ios-architectures/tree/master/ios-architectures/MVVM
MVPに似ています。
MVVMでもViewControllerをViewとして扱います。
MVPと同様に、ViewとModelの結合度が低いです。
ViewModelの役割
Viewで表示するために、APIやCoreData、Realmなどから必要なデータを取得、Viewに渡す仲介役。
Viewの状態も管理します。
Presenterと同様に、ライフサイクルやレイアウトに関する実装は含まず、UIKitには依存しません。
(サンプルコードではUIKitをimportしているのはViewだけになっています)
Presenterとの大きな違いは、Modelの変更を検知し、Viewに伝えるところです。(binding)
bindingはKVOやRxSwift、ReactiveCocoaなどのFRP (Functional Reactive Programming)をサポートするライブラリを使用して、通知することが多いです。
(サンプルコードではライブラリを使わず、didSet
とクロージャで実現しています)
特徴
状態管理をしているViewModelがModelを監視しているので、条件によってViewやコンテンツを出し分けする処理の見通しが良いです。
個人的評価
- わかりやすさ (使いやすさ)
- モデルを書き換えると自動でViewが更新されるので、更新処理がまとまりやすく、読みやすい
- 一方、変更の順序など追うのが難しい場合もある
- テストのしやすさ
- MVPと同じくらい
- 実装のしやすさ
- bindingのコードが多く出現するため、コード量がMVPよりも増えてしまうかもしれない
- FRPのライブラリを採用する場合は、その学習コストがかかる
VIPER
View Interactor Presenter Entity Routing
サンプルコード
https://github.com/tattn/ios-architectures/tree/master/ios-architectures/VIPER
役割
Interactor
Entityに関係するビジネスロジックやデータのフェッチ等の処理を行います。
Presenter
ビジネスロジックに関係するUIの操作を行います。
UIKitには依存しません。
Entity
データを表現するだけの役割。データの取得はInteractorで行う。
NSManagedObjectやRealmのObjectなどが該当する。
Router
画面遷移(seque)の管理を行う。
特徴
MV□系よりももっと役割がしっかりと分かれている。
例えば、データのフェッチはInteractorで行うことや、Entityというデータを表現するだけの役割が定められている。(MVCのModelより明らかに厳密)
また、Routerという画面遷移を制御する役割も存在している。
細かく分けられているため、
- 人によって分割方法が異なるということが減る
- テストもより、機能ごとに実施することができる
個人的評価
- わかりやすさ (使いやすさ)
- 役割が細かく分かれているため、コードの属人化は軽減でき、理解すれば読みやすい。
- テストのしやすさ
- protocolを利用しているため、モックに差し替えやすく、細かいテストが可能。
- 実装のしやすさ
- 各モジュール間をつなぐためのインターフェースが増えるため、コード量がMV□と比べて、格段に増える。
- インターフェースの形式やprotocolの命名などにブレが生じないように、プロジェクトで統一感をもたせる必要が出てくる。
参考
VIPER アーキテクチャにおけるiOSアプリの設計
http://www.slideshare.net/UsrNameu1/viper-arch
Clean Architecture
サンプルコード
https://github.com/tattn/ios-architectures/tree/master/ios-architectures/CleanArchitecture
https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
Clean Architectureに関しては有名なこちらのQiitaの記事がとても参考になります。
まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて
http://qiita.com/koutalou/items/07a4f9cf51a2d13e4cdc
特徴
かなり細かくレイヤー分けされていて、冗長化されています。
役割間をいかに疎結合にするかが重視されており、機能の追加などで副作用が伝搬しにくく、テストもしやすいです。
所感
実際に作ってみると、レイヤー間の接続のためのprotocolをどのように作るのが良いのか、悩みました。
悩む余地があるということは個人差が出てしまうということなので、実際に採用する場合はチームメンバーと認識をしっかり合わせる必要があると思います。
また、サンプルコードレベルのアプリでは冗長さで逆にわかりにくく感じるので、ある程度の規模で、長く運用するアプリ向けだと思います。
個人的評価
- わかりやすさ (使いやすさ)
- protocolと細かいクラス分けで冗長化されていて、分かりにくいかも
- 一度実装してしまえば、細かな機能の追加や変更はとてもやりやすい
- テストのしやすさ
- protocolでクラス間がつながっているので、差し替えやすく、機能ごとにもテストしやすい
- 実装のしやすさ
- protocolと細かいクラス分けでファイル数やコード量が格段に増える
- VIPERと同様、protocolの実装などにブレが生じないように、プロジェクトで統一感をもたせる必要が出てくる
Swiftでの設計の流行り
Swiftはprotocolがとても優秀なので、protocolを駆使した設計が多いです。
protocolをクラスに包含(composite)させることで、依存関係を曖昧にすることが可能です。
しかし、曖昧・冗長化されたアプリを人間がスムーズに読むことは難しいです。
次はこの分かりにくさという問題を改善した設計が出てくるのではないかなぁと思っていたりします。
Swiftは表現力が豊かでパワフルな素晴らしい言語です。
特にSwiftは型を大切にしていて、型を利用したPhantom TypeやType Erasureなどの技が話題になりました。
そこに何かヒントがあったりするのかなぁなどと妄想してます。
最後に
本当はFluxやRxSwift/Reactive Cocoaなどについても書きたかったですが、時間がありませんでした\(^o^)/
初のAdvent Calendarで、締切の大変さを実感しました がとても楽しく書けました!
こんな感じでSwift その2 Advent Calendar 2016の3日目はおしまいです!