はじめに
以前に書いた今更MVCとかでiOSアプリつくってみた(Swift)はなぜ MVC を導入するのかふわっとしてるし分け方もふわっとしててなんか違うなと思っていたのですが何が違うかわからず放置していました。
が、未だに LGTM ついたりしてまずいなと思い MVC について再考するために今回の記事を書きました。(google で「swift mvc」で検索するとわりと上位にくる。やっぱり Qiita の SEO すげぇ)
* アーキテクチャの起源は MVC であり他のアーキテクチャは MVC の課題を解決するために派生していったものであり MVC の理解がアーキテクチャの理解の第一歩と考え今更ですが MVC についての記事を書きました。
わりと長くなったのでめんどくさい人はソースだけでもどうぞ
長いの読みたくない人ように結論だけ
たぶん Cocoa MVC で一番重要なのは Model と View がそれぞれ独立してて再利用性が高いこと!!
つくったアプリ
成果物(Github)
livedoor天気のWeb API(商用利用不可)を利用した各都道府県の天気を表示するアプリ。
一覧 | 地方フィルター | お天気 |
---|---|---|
一覧
- 都道府県一覧を表示する
- お気に入り登録ができる
- お気に入りで絞り込み表示ができる
- 地方で絞り込み表示ができる
- 都道府県選択でお天気を取得してお天気画面に遷移する
地方フィルター
- 地方で絞り込み表示ができる
お天気
- 3日分の天気を表示する
- リフレッシュボタンで天気を再取得
前提
なぜアーキテクチャを導入するのか?
アーキテクチャを導入する理由は下記の4つを実現するためだと考えています。(個人の意見です)
- テストを書くため(デグレを防ぐ)
- パーツの再利用性を高めるため(変更が容易になる)
- 変更を織り込んだ構造にするため(ライブラリなどの移行が楽になる)
- チームでの分業をするため(コンフリクトを防ぐ)
とりあえず何も切り分けずに ViewController (以下 VC) 3つのみで作ったのが下記。
fat_fat_fat(これはこれで美しい気がする)
こいつについて上記4項目をみていくと
- テストを書くため(デグレを防ぐ)
ほぼすべてのプロパティ、メソッドがprivate
なのでテストが全く書けない。何か改修が入った場合は実機テストで以前と同一の動作をするか目視で確認する必要がある。 - パーツの再利用性を高めるため(変更が容易になる)
全く同じ画面をつくることは可能だがそれ以外再利用はできない。同じような画面を作りたい場合、コードをコピペしてカスタムするしかない。 - 変更を織り込んだ構造にするため(ライブラリなどの移行が楽になる)
通信部分にAlamofire
などのライブラリを導入したいとなった場合、通信処理が散見しており導入するのに複数箇所(クラス)を書き換える必要がある。 - チームでの分業をするため(コンフリクトを防ぐ)
一覧画面で複数の機能改修があった場合、同じPrefectureListViewController
を触る必要がありチームで分業するには細心の注意を払う必要がある。
長期的な保守を考えるとアーキテクチャを導入していない VC 3つ構成は後々苦労しそうなことがわかる。
仮に「明日のイベントだけで使うアプリが必要」など保守が全くいらないのであればアーキテクチャの導入とかはいらないのかもしれない。(本当に保守しないかは言質を取る必要がある)
アーキテクチャを導入する理由について、以前はファイルの行数を減らすため!とか考えていたためとりあえず Model と View と Controller にわけて 5000 行を超えるファイルがあれば 2500 行以降をカテゴリに分けよう!みたいなことが平気で行われていた。
こういった意味不明な考えで分割しても処理があっちこっちにとぶだけでソースの難読化を進めるだけなことが多い。むしろ分けない方が読みやすい場合さえある。分割するには明確な目的をもって分割することが重要である。
MVCの派生について
とりあえず「アーキテクチャ」「MVC」などの言葉を覚えてネットで検索すると色々な記事がヒットする。そして読んでみると言ってることがそれぞれ微妙に違う。。。結局MVCってなんなんだ?という壁にぶち当たる。(そうして私はよくわからないまま以前の記事を書いた)
なぜこのようなことが起こるのか?
MVC はもともと Smalltalk の開発環境で発案された。Smalltalkでの開発環境のために考えられているのでそのまま別のプラットフォームで導入するとうまく機能しなくなる。そこでそれぞれのプラットフォームに最適化されプラットフォームごとに MVC の違いが生まれた。起源は同じだがそれぞれのプラットフォームの MVC は違うものであり、同じ MVC と考えて議論すると齟齬が生じるのは当然なのである。(原作のナパームストレッチ (A) とアニメ版ナパームストレッチ (X) の話が噛み合わないことと同様)
iOS の MVC (以下 Cocoa MVC) も iOS 用に最適化されていて、本来の MVC とは構成が異なっている。そのため Cocoa MVC は MVP (Passive View) ではないか?と言われることもある。
プラットフォームごとに違いがあることはわかった。しかし、iOS の MVC だけで調べてみてもそれぞれ違う主張をしていることがある。これはなぜか?想像だがもともと Cocoa MVC でやっていたが何かしらの課題にぶつかりそこから独自に構成を変えていって異なった構成になったのではないかと考えられる。
では MVC とはなんなのか?iOSアプリ設計パターン入門という名著に答えが書いてあった。
実は、Reenskaug 氏の 2006 年の総括によれば、MVC の目的はオブザーバー同期ではなく「メンタルモデルとモデルを一致させること」だといいます。
その意味では、Cocoa MVC も MVC だといえます。MVC は、あなたの心にあるのです。
*76ページ、コラム:Cocoa MVC は MVC ? より引用
つまりそれぞれ構成が違ってもメンタルモデルとモデルが一致すれば MVC なんだ!!主張が異なる iOS の MVC も Cocoa MVC ではないというだけで作生者が MVC といえば MVC なんだ。(MVC は、あなたの心にあるのです。)
MVCとは
前項でも述べたが iOS の MVC でもそれぞれ主張が異なる。本記事では共通認識としての Cocoa MVC について記載する。
とりあえず Cocoa MVC については下記を参考にするのがいいだろう。
Model-View-Controller
ざっと読んだはいいが今の私にはまだハードルが高かったようだ。(英語って難しい)
しかし、他の日本語記事を参考にするのも危険だ。色々悩んだ末、下記の本をみつけた。
iOSアプリ設計パターン入門
こちらは Qiita でもよく目にする方々7人によって書かれている。個人で書いた記事よりも信憑性は高いはずということでこちらの本を参考に Cocoa MVC について学ぶことにした。(つまりこんな記事を読むよりもこの本を読もう!)
原初MVC
前項でも述べたが MVC はもともと Smalltalk の開発環境で発案された。全部一箇所に記述していたものを「UI に関係するロジック (Presentation 以下 P) 」と「それ以外 (Domain ≒ Model 以下 D と M) 」で分ける Presentation Domain Separation (PDS) という考えが生まれ、その後 P をさらに 入力 (Controller 以下 C) と 出力 (View 以下 V) に分けようという考えが生まれた。これが原初 MVC である。
これを iOS で無理やりあてはめようとすると下記のような構成になる。(ViewController は M でも V でも C でもないなにからしい)
*詳しくは「第 4 章 アーキテクチャのパターンを鳥瞰するの4.1から4.2.1まで」と「第 5 章 MVCの5.1から5.1.1まで」と「5.2.1 原初 MVC」
Cocoa MVC
前項の原初 MVC には下記のような課題がある。
- プレゼンテーションロジックの記載場所が不明確。
V に書くのか C に書くのかよくわからない。 - プレゼンテーションロジックのテストがしにくい。
V か C のテストが必要でテストしようと思うとそれぞれ M, C と M が必要になってくる。またその部分のプロパティ、メソッドがpublic
である必要がある。 - M 以外の再利用性がない。
V は M と C に依存しており、C は M に依存しているので V と C の再利用性が低い。(M だけは独立しているので再利用性が高い)
Cocoa MVC では C の再利用性を捨てて M と V の再利用性を高めるため下記のような構成になった。
これにより原初 MVC の 1. と 3. の課題は解決した!!(2. は C のテストがいるので難しい)
*詳しくは「第 4 章 アーキテクチャのパターンを鳥瞰するのp.60 Cocoa MVC は MVP (Passive View)」と「第 5 章 MVCの5.1.2 Cocoa MVC」と「5.2.2 Cocoa MVC」
お天気アプリ実装
MVC の考え方をわかった気になったところでいよいよ実装にはいっていきます。
VC3つのみでつくったやつを MVC に分けていきます。
Cocoa MVC で重要なのは M と V に再利用性があること!M は V も C も知らないし、V は M も C も知らない。これを徹底するため Model と View はそれぞれ別モジュールに分割することにしました。V と M を import するのは C のみなので下記のような Xcode テンプレートを作成しました。
テンプレートの作成方法は下記記事参考
Xcodeのファイルテンプレートを自作する
Controller テンプレート
//
// ___FILENAME___
// ___PROJECTNAME___
//
// Created by ___FULLUSERNAME___ on ___DATE___.
//___COPYRIGHT___
//
import UIKit
import Models
import Views
final class ___FILEBASENAMEASIDENTIFIER___: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
MVC による分割
View
View がやるのは見た目に関すること。fat_fat_fatの下記の処理とかだろう。
- View の設定 (
addSubViews()
の部分など) - View のレイアウト設定 (
setupLayout()
の部分)
これらの処理を xib と UIView のサブクラスで実装する。
Controller
Controller がやるのは M と V の仲介とプレゼンテーションロジックと画面遷移系(これは VC が C である以上こいつがやるしかない)。fat_fat_fatの下記の処理とかだろう。
- ボタンとかのアクション実装
- 画面遷移などの Show~ 系
- 表示用のデータ加工(ex. 最高気温とかを加工して 30℃ とかにする)
- プレゼンテーション関連の状態保持(ex. ボタンのチェック状態の保持とか)
- M の更新を監視する(Delegate じゃないのは複数の C に通知するため?ここちょっとわかってない...)
- V の表示更新
以前はボタンとかのアクション実装に関して、V に定義してデリゲートで C に伝えるとかをしていた。(こんな感じ)これは V からのアクション伝達ということに囚われ過ぎていたのと V のプロパティを公開したくなかったのが原因だと思われる。しかし、前者は C に IBAction を直接設定したとしても UIButton (V) から sendAction
で C に伝わるので V -> C は実現できていると考えることができる。後者は V を知っているのは superView もしくは C しかいないので公開されていても何ら問題はない。問題が起きるとすればそいつが V の参照を持っていることがおかしいと考えるべきである(つまり構造に問題がある)。
Model
Model がやるのは UI 関係の処理以外すべて。fat_fat_fatの下記の処理とかだろう。
- 天気の取得(通信まわり)
- お気に入りの保存
- 都道府県の情報取得
- どんなデータを受け取るかの表現([String: Any] で受けているところを Weather などの型で表現する)
- 都道府県のフィルター処理(これはもしかしたら P ロジックで C がやるべきかもしれない)
- データが更新されたら C に通知する
さらに分割
上記の分割だけでも十分な気がするが、もう一度アーキテクチャ導入の意義について考えてみたい。(ちょっとここからは MVC とは関係ないかも知れない)
- テストを書くため(デグレを防ぐ)
- パーツの再利用性を高めるため(変更が容易になる)
- 変更を織り込んだ構造にするため(ライブラリなどの移行が楽になる)
- チームでの分業をするため(コンフリクトを防ぐ)
MVC による分割で 1. 2. 4 はある程度実現できたと考えていいだろう。しかし、3. についてはもう少し考慮が必要である。
プログレス表示
例えば、プログレス表示に SVProgressHUD を利用する場合そのまま使うと使いたい VC で毎回 import する必要がある。
プログレスの表示は様々な VC で利用することが考えられるので VC の extension として showProgress
、hideProgress
に切り出す。これにより import SVProgressHUD
はこのエクステンションファイルのみでよくなった。今後、OS のアップデートなどで SVProgressHUD
によるプログレスの表示がうまくいかなくなり、他のライブラリに移行したいとなった場合、このエクステンションファイルの修正のみで済む。(実際 SceneDelegate のせいか表示がおかしくなり PKHUD に切り替える際恩恵を受けれた)
データの保存
現状お気に入りの保存には UserDefaults
を利用しているが今後の対応で CoreData や Realm など他の保存方法に移行したくなることがあるかも知れない。直接 UserDefaults
を呼び出していると移行の際に複数のファイルを修正することになる。
データの保存・取得処理を下記のようにプロトコルとして切り出す。
データの保存・取得処理
public protocol FavoritePrefectureDataStore {
typealias Object = String
func fetchAll() -> [Object]
func add(_ objects: [Object]) -> Result<[Object], FavoritePrefectureDataStoreError>
func remove(_ objects: [Object]) -> Result<[Object], FavoritePrefectureDataStoreError>
}
この切り出しにより呼び出し側は FavoritePrefectureDataStore
型を操作するだけなので保存先が UserDefaults
か Realm かなどを知る必要がなくなり、Realm に移行したい場合も FavoritePrefectureDataStoreImpl
の修正だけで済むだろう。(通信部分に Alamofire を利用しているがこちらも似たような感じでプロトコルに切り出していき他のライブラリへの移行が APIClient.swift のみの修正で済むようになっている)
上記のように分割していった結果、下記が完成した
お天気アプリ
テストもまずまずだと思う。
これで MVC の導入は終わった。しかし、 MVC の下記の課題は残ったままである。
2. プレゼンテーションロジックのテストがしにくい。
これを解決するために MVP とかにつながっていくんだろう...
おわりに
以上が現時点での私の MVC の認識です。今後やっぱちがうなとか思ったら都度修正していきます
長々と記事を書いたが、ネットの記事をみて Cocoa MVC を学ぼうとするのは危険である(この記事も間違っている可能性がある)。おとなしくiOSアプリ設計パターン入門を読もう
記事を書いたのは他の人の意見も聞きたいなと思ったからなので「その認識で合ってるよ」とか「全然ちゃうわ。頭わいてんのか?」など優しく指摘してくださると幸いです
20200415追記 MVPも書いてみました
20200422追記 MVVMも書いてみました