イントロダクション
アプリを作る際に考えることは色々ありますが、その中でも特に重要なのが設計パターンの選定だと思います。
私自身は今までMVVMパターンでの実装が主だったのですが、MVPについて前から興味があったのでサンプルアプリを作りながら両者のメリデメを考察してみました。
参考のためiOSアプリ設計パターン入門を読みました。各章の内容が非常に簡潔にわかりやすくまとまっており、読みやすいためかなりオススメな著書です。
MVVMとは
一言で言うとGUIの構造をModel, View, ViewModelの3つに分けたパターンです。
それぞれの役割は以下です。
- Model - 表示に関わらないドメインロジックを持つ
- View - ユーザー操作を受けつけ、ViewModelの状態の変更を監視し画面の更新を行う
- ViewModel - 画面の表示全般に関わるロジックを持ち、Viewに使うデータを保持する
図のように参照関係は一方向で、逆の参照関係はありません。Model, ViewModelの変更はイベントにより通知されます。
MVVMパターンの大きな特徴としてはViewの更新をデータバインドによって行うという点です。
手続き的ではなく宣言的にロジックが表現できるため、コンポーネント間を疎結合に保てます。それによって可読性やテスト容易性を向上させます。
このデータバインド機構にRxSwiftやReactiveSwitといったライブラリを利用するのですが、こちらの概念理解にかかるコストが非常に高いというのも特徴の一つです。
MVPとは
こちらも一言で言うとGUIの構造をModel, View,Presenterの3つに分けたパターンです。
Modelの役割はMVVMと変わりません。
- Model - 表示に関わらないドメインロジックを持つ
- View - ユーザー操作を受けつけ、Presenterに処理を委譲。Presenterから画面更新の指示を受け付け、画面を更新する
- Presenter - ModelとViewの仲介役。画面表示に関するロジックを持つ
Viewの更新にはPassive ViewとSupervising Controllerの2つの方法がありますが、今回は図のPassive Viewを前提にします。Passive Viewはデータフローが追いやすい反面、簡単な処理もModelが絡んだ際は必ず以下のフローに準拠する必要があります。
View → Presenter → Model → Presenter → View
そのため、コードが冗長になりやすいという面もあります。
しかし、全てのプレゼンテーションロジックをPresenterの中に閉じ込めることができるのでテスト容易性が向上します。
MVVMとMVPの比較
まず、MVVMもMVPも共通して言えることはコンポーネントを疎結合にしてテスト容易性や可読性を向上させています。
そもそもなぜそうする必要があるのか。
それはAppleが提唱したCococa MVCパターンにてViewControllerの役割が大きすぎて可読性、テスト容易性が低くなってしまい、いわゆるFat View Controllerになりがちということがあります。
そのため、MVVMやMVPなどのパターンを使い、責務の分離をすることでそれらの課題を解決しています。
MVVMとMVPの違いを実感するために冒頭で紹介した著書のサンプルコードを元にMVVMとMVPそれぞれの設計でサンプルアプリを作ってみました。
Githubのユーザーを検索し、ユーザーのリポジトリ一覧を表示するものです。
データは実際のAPI通信は行わず、モックから取得しています。
ユーザー検索 | リポジトリを表示 |
---|---|
本記事ではsearchBarに文字を入力して対象のユーザーを表示する部分をMVVM, MVPそれぞれのパターンで実装する部分を解説します。リポジトリの表示についてはサンプルコードのURLを最後に記載しますので、興味を持たれた方は見て頂けますと幸いです。
まずはMVPでの実装を見てみます。
Presenterの実装ではまずInput/Outputの処理をプロトコルで宣言しておきます。
ViewControllerから委譲されたInputの処理を行い、OutputにてModelに処理を委譲し、結果を再びViewControllerに伝えます。
// SearchUserPresenter
protocol SearchUserPresenterInput {
var numberOfUsers: Int { get }
func user(forRow row: Int) -> User?
func didSelectRow(at indexPath: IndexPath)
func didTapSearchButton(text: String?)
}
protocol SearchUserPresenterOutput: AnyObject {
func updateUsers(_ users: [User])
func transitionToUserDetail(userName: String)
}
final class SearchUserPresenter: SearchUserPresenterInput {
...
func didTapSearchButton(text: String?) {
guard let query = text { return }
model.fetchUser(query: query) { [weak self] result in
switch result {
case .success(let users):
self?.users = users
DispatchQueue.main.async {
self?.view.updateUsers(users)
}
case .failure:
()
}
}
}
...
}
ViewControllerはOutputプロトコルを実装することで画面を更新します。
// SearchUserViewController.swift
extension SearchUserViewController: SearchUserPresenterOutput {
func updateUsers(_ users: [User]) {
tableView.reloadData()
}
...
}
これによって先ほど説明した以下フローを実現しています。
View → Presenter → Model → Presenter → View
次にMVVMでの実装を見てみます。
ViewModelはViewからの入力に反応してstate(Mutable Property)を更新します。
ここではViewからsearch()メソッドが実行(入力)されることによってModelがデータ取得処理を行い、結果をstateにバインドしています。
final class SearchUserViewModel {
let state = BehaviorRelay<[User]?>(value: nil)
private let model: SearchUserModel
private let disposeBag = DisposeBag()
init(model: SearchUserModel) {
self.model = model
}
func search(query: String) {
model.fetch(query: query)
.asObservable()
.bind(to: state)
.disposed(by: disposeBag)
}
}
Viewでは予め監視対象を設定しておき、ViewModelのイベント通知によって画面更新を行います。
final class SearchUserViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
viewModel = SearchUserViewModel(model: SearchUserModel())
tableView.registerCell(type: SearchUserCell.self)
viewModel.state.subscribe(onNext: { [unowned self] _ in
self.tableView.reloadData()
}).disposed(by: disposeBag)
}
}
...
extension SearchUserViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let state = viewModel.state.value else { return 0 }
return state.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueCell(type: SearchUserCell.self, indexPath: indexPath)
guard let state = viewModel.state.value else { return cell }
cell.configure(user: state[indexPath.row])
return cell
}
}
MVVMとMVPそれぞれで実装してみての所感
まず、最初に感じたのはMVPではプレゼンテーションロジックの実装を行う際に、「ViewとModelそれぞれのことを考えなければならない。」ということが気になりました。
先に説明した通りPresenterは「仲介役」です。
そのため、Presenterを実装する際にはViewとModelそれぞれの顔色を伺いながら実装するということを余儀なくさせられます。
一方で、MVVMはプレゼンテーションロジックを実装する際にはあくまでModelからとってきたデータをどのように整形するのかのみに集中できました。
コードを見てもらえばわかる通り、コード量も段違いにMVPの方が多いです。
これは先に説明した通りコードが冗長になるということを体現しています。
まとめ
今回実際にMVVM, MVPパターンでサンプルアプリを実装してみてそれぞれのメリット・デメリットを比較したものは以下でした。
MVVM
メリット
- View, ViewModel, Modelが一方向の参照関係になっているため、疎結合が保て、それぞれの作業に集中できる
- それぞれのコンポーネントで作業分担が明確なため、冗長なコードが生まれず、コード量を比較的少なくできる
デメリット
- RxSwift, ReactiveSwiftなどのライブラリが必須なため、初期設定が手間
- FRPの概念を理解するまでの学習コストが高い
MVP
メリット
- 初期導入に必要なものがない
- 特別な実装が不要なため、すでにMVCなどで実装しているプロジェクトにも導入がしやすい
デメリット
- データフローへの準拠が厳しく、コードが冗長になりやすい
- コード量が増え、実装に手間がかかる