【iOSDC2017】MVC→MVP→MVVM→Fluxの実装の違いを比較してみる

# はじめに
iOSDC2017にて[MVC→MVP→MVVM→Fluxの実装の違いを比較してみる](https://iosdc.jp/2017/node/1396)という内容で、Githubのユーザー検索のデモアプリをベースにした発表資料で登壇させていただきました。
登壇枠が15分だったため、ViewControllerを跨いだ（FavoriteViewController <-> RepositoryViewController）4つのデザインパターンの実装の違いにフォーカスした内容となっているので、画面遷移やテストの書き方などについても補足説明を書いていきます。
※MVP、MVVM、Fluxでの補足説明という形で書いていきます。
登壇資料は下記になります。

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">本日のプレゼン資料、アップしました。<a href="https://t.co/TIecyULVp4">https://t.co/TIecyULVp4</a><a href="https://twitter.com/hashtag/iosdc?src=hash">#iosdc</a></p>&mdash; marty-suzuki (@marty_suzuki) <a href="https://twitter.com/marty_suzuki/status/909316660372291584">September 17, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

また登壇時に利用したサンプルソースは、こちらのリポジトリで公開されています。
https://github.com/marty-suzuki/iOSDesignPatternSamples

デモアプリのアプリの動きはこのようになります。

![app.gif](https://qiita-image-store.s3.amazonaws.com/0/60325/e17b8261-1f5b-18ac-cf0c-8532dcc94541.gif)

- SearchViewController... 文字列検索でGithub上のユーザーのリストを取得し、表示します。
- UserRepositoryViewController... SearchViewControllerで選択されたユーザーのリポジトリ一覧を取得し、表示します。
- RepositoryViewController... UserRepositoryViewControllerまたはFavoriteViewControllerで選択されたリポジトリをSafariViewで表示します。また、右上のお気に入りボタンから、レポジトリをFavoriteViewControllerに追加・削除ができます。
- FavoriteViewController... お気に入りされたリポジトリの一覧が表示されます。

![structure.png](https://qiita-image-store.s3.amazonaws.com/0/60325/e5915a99-2e14-22aa-900f-6380351961fe.png)

## MVP

それでは、まずMVPによる実装について書いていきます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/60325/fbb64d9f-1167-f0c0-aa72-5e17f6bb1fa3.jpeg" width="50%">

ViewControllerに実装されるプレゼンテーション層に関するロジックをPresenterに実装することで、責務を明確にすることができます。
流れとしては、Viewはユーザーからのアクションを受け取ってPresenterに渡します。
そして、そのアクションによってPresenterはModelの取得や更新を行います。
Presenterは更新された情報をもとにViewの反映メソッドを呼び、Viewに更新結果を反映させます。

### 画面遷移

`UITableViewDataSource`や`UITableViewDelegate`の実装を、`FavoriteViewController`の場合は`FavoriteViewDataSource`に移しています。
`FavoriteViewDataSource`内の`tableView(_:didSelectRowAt:)`では、`FavoritePresenter`の`showFavoriteRepository(at:)`が呼び出されています。

```swift:FavoriteViewDataSource
final class FavoriteViewDataSource: NSObject, UITableViewDelegate, ... {
    private let presenter: FavoritePresenter
    
    init(presenter: FavoritePresenter) {
        self.presenter = presenter
    }
    ...
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        presenter.showFavoriteRepository(at: indexPath.row)
    }
    ...
}
```

`FavoritePresenter`はprotocolとなっていて、実装がされてるのは`FavoriteViewPresenter`になります。
`showFavoriteRepository(at:)`が呼び出されると、presenter内で保持している`GithubKit.Repository`の配列から該当の`GithubKit.Repository`を取得し、`FavoriteView`の`showRepository(with:)`を呼び出しています。

```swift:FavoritePresenter
protocol FavoritePresenter: class {
    init(view: FavoriteView)
    ...
    func showFavoriteRepository(at index: Int)
    ...
}

final class FavoriteViewPresenter: FavoritePresenter {
    private weak var view: FavoriteView?
    private var favorites: [GithubKit.Repository] = [] {
        didSet {
            view?.reloadData()
        }
    }
    ...
    init(view: FavoriteView) {
        self.view = view
    }
    ...
    func showFavoriteRepository(at index: Int) {
        let repository = favorites[index]
        view?.showRepository(with: repository)
    }
}
```

`FavoriteView`はprotocolとなっていて、実装がされてるのは`FavoriteViewController`になります。
`showRepository(with:)`が呼び出されると`RepositoryViewController`を初期化し、navigationControllerでpushすることで遷移させます。

```swift:FavoriteView
protocol FavoriteView: class {
    ...
    func showRepository(with repository: GithubKit.Repository)
}

final class FavoriteViewController: UIViewController, FavoriteView {
    @IBOutlet weak var tableView: UITableView!
    
    private(set) lazy var presenter: FavoritePresenter = FavoriteViewPresenter(view: self)
    private lazy var dataSource: FavoriteViewDataSource = .init(presenter: self.presenter)
    ...
    func showRepository(with repository: GithubKit.Repository) {
        let vc = RepositoryViewController(repository: repository, favoritePresenter: presenter)
        navigationController?.pushViewController(vc, animated: true)
    }
    ...
}
```

上記のTableViewのCellが選択されてから、次のViewControllerに遷移するまでの流れは下図のようになります。

![MVP.002.jpeg](https://qiita-image-store.s3.amazonaws.com/0/60325/3866220b-47f2-212c-85ce-8ffd390a405f.jpeg)

### お気に入りに追加

favoriteButtonItemがタップされると`favoriteButtonTap(_:)`が呼び出されるので、`RepositoryPresenter`の`favoriteButtonTap()`を呼び出します。
また、presenterのviewがinitializerの引数に含まれていない理由としては、repositoryとfavoritePresenterをViewControllerで保持せずに済むようにするためです。

```swift:RepositoryView
protocol RepositoryView: class {
    ...
}

final class RepositoryViewController: SFSafariViewController, ... {
    private(set) lazy var favoriteButtonItem: UIBarButtonItem = {...}()
    private let presenter: RepositoryPresenter
    
    init(repository: GithubKit.Repository,
         favoritePresenter: FavoritePresenter) {
        self.presenter = RepositoryViewPresenter(repository: repository,
                                                 favoritePresenter: favoritePresenter)
        super.init(url: repository.url, entersReaderIfAvailable: true)
        self.presenter.view = self
    }
    ...
    @objc private func favoriteButtonTap(_ sender: UIBarButtonItem) {
        presenter.favoriteButtonTap()
    }
    ...
}
```

`RepositoryPresenter`の初期化時にViewも引数として含めたい場合は、下記のようにLazy Initializationを利用することで実現可能です。
しかしこの場合に、`RepositoryPresenter`内でrepositoryとfavoritePresenterを保持するにも関わらず、ViewControllerでも保持してしまうという問題もあるかと思います。

```swift:RepositoryViewController
final class RepositoryViewController: SFSafariViewController, ... {
    ...
    private(set) lazy var presenter: RepositoryPresenter = {
        return RepositoryViewPresenter(repository: self.repository,
                                       favoritePresenter: self.favoritePresenter,
                                       view: self)
    }()
    private let repository: Repository                           
    private let favoritePresenter: FavoritePresenter

    init(repository: Repository,
         favoritePresenter: FavoritePresenter) {
        self.repository = repository,
        self.favoritePresenter = favoritePresenter
        super.init(url: repository.url, entersReaderIfAvailable: true)
    }
    ...
}
```

`RepositoryPresenter`はprotocolとなっていて、実装がされてるのは`RepositoryViewPresenter`になります。
`favoriteButtonTap()`が呼ばれると、`RepositoryViewPresenter`で保持している`Repository`が`FavoritePresenter`内に含まれているかを確認し、その結果によってお気に入りの一覧に追加または削除をしています。

```swift:RepositoryPresenter
protocol RepositoryPresenter: class {
    init(repository: GithubKit.Repository, favoritePresenter: FavoritePresenter)
    weak var view: RepositoryView? { get set }
    ...
    func favoriteButtonTap()
}

final class RepositoryViewPresenter: RepositoryPresenter {
    weak var view: RepositoryView?
    private let favoritePresenter: FavoritePresenter
    private let repository: GithubKit.Repository
    ...
    init(repository: GithubKit.Repository, favoritePresenter: FavoritePresenter) {
        self.repository = repository
        self.favoritePresenter = favoritePresenter
    }
    
    func favoriteButtonTap() {
        if favoritePresenter.contains(repository) {
            favoritePresenter.removeFavorite(repository)
            ...
        } else {
            favoritePresenter.addFavorite(repository)
            ...
        }
    }
}
```

`FavoritePresenter`はprotocolとなっていて、実装がされてるのは`FavoriteViewPresenter`になります。
お気に入りの一覧を保持している`favorites: [GithubKit.Repository]`が更新されると、`FavoriteView`の`reloadData()`を呼び出します。

```swift:FavoritePresenter
protocol FavoritePresenter: class {
    ...
    func addFavorite(_ repository: GithubKit.Repository)
    func removeFavorite(_ repository: GithubKit.Repository)
    func contains(_ repository: Repository) -> Bool
    ...
}

final class FavoriteViewPresenter: FavoritePresenter {
    private weak var view: FavoriteView?
    private var favorites: [GithubKit.Repository] = [] {
        didSet {
            view?.reloadData()
        }
    }
    ...
    func addFavorite(_ repository: GithubKit.Repository) {
        if favorites.lazy.index(where: { $0.url == repository.url }) != nil {
            return
        }
        favorites.append(repository)
    }
    
    func removeFavorite(_ repository: GithubKit.Repository) {
        guard let index = favorites.lazy.index(where: { $0.url == repository.url }) else {
            return
        }
        favorites.remove(at: index)
    }
    
    func contains(_ repository: GithubKit.Repository) -> Bool {
        return favorites.lazy.index { $0.url == repository.url } != nil
    }
    ...
}
```

`reloadData()`の中では`tableView?.reloadData()`が実行され、お気に入り一覧の画面が更新されます。

```swift:FavoriteView
protocol FavoriteView: class {
    func reloadData()
    ...
}

final class FavoriteViewController: UIViewController, FavoriteView {
    @IBOutlet weak var tableView: UITableView!
    
    private(set) lazy var presenter: FavoritePresenter = FavoriteViewPresenter(view: self)
    ...
    func reloadData() {
        tableView?.reloadData()
    }
}
```

お気に入りに追加/削除される流れは下記のようになります。

`FavoritePresenter`を所有しているのは`FavoriteViewController`ですが、ViewControllerが遷移する際に`FavoritePresenter`の参照が渡されていきます。
`RepositoryViewController`でお気に入りにボタンがタップされた際に、`FavoritePresenter`内で保持している`GithubKit.Repository`の配列の状態によって`RepositoryViewController`の`Repository`が追加または削除されます。
`Repository`の配列に変更があると`view?.reloadData()`が実行されるので、`FavoriteViewController`上でリストが更新されます。

![MVP.003.jpeg](https://qiita-image-store.s3.amazonaws.com/0/60325/74bea653-0897-db1b-ce93-a72784dd2ef9.jpeg)

### テスト

`FavoriteView`はprotocolなので、モック化にすることで`FavoritePresenter`のテストを行うことができます。
`FavoriteViewMock`は下記のようになります。
該当するメソッドが呼び出されたとき用のclosureをpropertyで定義し、それぞれのメソッドが呼び出された際に実行します。

```swift:FavoriteViewMock
class FavoriteViewMock: FavoriteView {
    var presenter: FavoritePresenter?
    
    var didCallReloadData: (() -> Void)?
    var didCallShowRepository: ((GithubKit.Repository) -> Void)?
    
    func reloadData() {
        didCallReloadData?()
    }
    
    func showRepository(with repository: GithubKit.Repository) {
        didCallShowRepository?(repository)
    }
}
```

XCTestなどでテストを実行する際の実装は、下記のようになります。

`FavoriteViewPresenter`のinitializerの引数であるviewは`FavoriteView`となっているので、`FavoriteView`を採用している`FavoriteViewMock`を引数として渡すことが可能です。
テストメソッド内では該当するclosureに対して期待する結果を記述し、その後に紐づくpresenterのメソッドを呼び出しています。

```swift:FavoriteViewTests
class FavoritePresenterTests: XCTestCase {
    var favoriteView: FavoriteViewMock!
    var favoritePresenter: FavoritePresenter!
    
    override func setUp() {
        super.setUp()
    
        self.favoriteView = FavoriteViewMock()
        self.favoritePresenter = FavoriteViewPresenter(view: self.favoriteView)
        self.favoriteView.presenter = favoritePresenter
    }
    
    func testReloadData() {
        let expectation = self.expectation(description: "testReloadData expectation")

        favoriteView.didCallReloadData = {
            expectation.fulfill()
        }

        let repository = GithubKit.Repository.mock()
        favoritePresenter.addFavorite(repository)

        waitForExpectations(timeout: 1, handler: nil)
    }
    
    func testShowRepository() {
        let expectation = self.expectation(description: "testShowRepository expectation")

        let repository = GithubKit.Repository.mock()
        favoritePresenter.addFavorite(repository)

        favoriteView.didCallShowRepository = { repo in
            XCTAssertEqual(repository.url, repo.url)
            expectation.fulfill()
        }

        favoritePresenter.showFavoriteRepository(at: 0)

        waitForExpectations(timeout: 1, handler: nil)
    }
}
```

### 考察

ViewとPresenterはお互いの参照も持ち合うので、依存し合う形になります。
しかし、PresenterとViewを抽象化しておくことでMock化が容易になります。
よって、PresenterにViewのMockを渡すことプレゼンテーション層に関するロジックのテストが可能になります。

## MVVM

続いてMVVMによる実装について書いていきます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/60325/24a63bd4-c047-8208-2296-7a6db38fd690.jpeg" width="50%">

ViewControllerに実装されるプレゼンテーション層に関するロジックをViewModelに実装することで、責務を明確にすることができます。
流れとしては、Viewはユーザーからのアクションを受け取って、そのイベントをViewModelにbindします。
そして、そのbindされたイベントによってViewModelはModelの取得や更新を行います。
ViewModelは更新された情報をイベントとしてViewにbindし、Viewはbindされたイベントをもとに結果を反映させます。

※Data bindingを実現するために、[RxSwift](https://github.com/ReactiveX/RxSwift)を利用しています。

### 画面遷移

`UITableViewDataSource`や`UITableViewDelegate`の実装を、`FavoriteViewController`の場合は`FavoriteViewDataSource`に移しています。
`FavoriteViewDataSource`内の`tableView(_:didSelectRowAt:)`では、`_selectedIndexPath: PublishSubject<IndexPath>`が呼び出されています。
そして、`selectedIndexPath: Observable<IndexPath>`によって外部に公開されています。

```swift:FavoriteViewDataSource
final class FavoriteViewDataSource: NSObject, UITableViewDelegate, ... {
    let selectedIndexPath: Observable<IndexPath>
    private let _selectedIndexPath = PublishSubject<IndexPath>()
    private let viewModel: FavoriteViewModel
    
    init(viewModel: FavoriteViewModel) {
        self.viewModel = viewModel
        self.selectedIndexPath = _selectedIndexPath
    }
    ...
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        _selectedIndexPath.onNext(indexPath)
    }
    ...
}
```

`selectedIndexPath: Observable<IndexPath>`を`FavoriteViewModel`内のinputとして、initializerの引数として受け取ります。
そのselectedIndexPathからイベントが渡ってくると、ViewModel内で保持している`_favorites: Variable<[GithubKit.Repository]>`の最新の状態をwithLatestFromで取得し、配列内で該当するrowのRepositoryを返します。
そのイベントを`selectedRepository: Observable<GithubKit.Repository>`として外部に公開します。
また`favorites: Observable<[GithubKit.Repository]>`は`_favorites`のイベントを外部に公開するために利用しています。

```swift:FavoriteViewModel
final class FavoriteViewModel {
    let favorites: Observable<[GithubKit.Repository]>
    let selectedRepository: Observable<GithubKit.Repository>
    ...
    private let _favorites = Variable<[GithubKit.Repository]>([])
    ...
    init(..., selectedIndexPath: Observable<IndexPath>) {
        self.favorites = _favorites.asObservable()
        self.selectedRepository = selectedIndexPath
            .withLatestFrom(_favorites.asObservable()) { $1[$0.row] }
        ...
    }
}
```

`FavoriteViewDataSource`でも`FavoriteViewModel`を利用しているので、`FavoriteViewModel`のinitializerの引数でdataSource.selectedIndexPathを渡すことができません。
そのため、`FavoriteViewController`でも`selectedIndexPath: PublishSubject<IndexPath>`定義し、そのselectedIndexPathを`FavoriteViewModel`のinitializerに渡します。
dataSource.selectedIndexPathをself.selectedIndexPathにbindすることで、dataSourceからのイベントをviewModelに流すことができます。
また、`FavoriteViewModel`で公開されているselectedRepositoryを`showRepository: AnyObserver<GithubKit.Repository>`にbindすることで、`RepositoryViewController`に遷移させることができます。
また、self.favoritesを`favoritesInput: AnyObserver<[GithubKit.Repository]>`として外部に公開し、viewModel.favoritesを`favoritesOutput: Observable<[GithubKit.Repository]>`として外部に公開します。
これらが遷移する際に、それぞれのViewControllerのinitializerの引数として渡されていきます。

```swift:FavoriteViewController
final class FavoriteViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!

    var favoritesInput: AnyObserver<[GithubKit.Repository]> { return favorites.asObserver() }
    var favoritesOutput: Observable<[GithubKit.Repository]> { return viewModel.favorites }

    private lazy var dataSource: FavoriteViewDataSource = .init(viewModel: self.viewModel)
    private private(set) lazy var viewModel: FavoriteViewModel = {
        .init(..., selectedIndexPath: self.selectedIndexPath)
    }()
    ...
    private let selectedIndexPath = PublishSubject<IndexPath>()
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        // observe dataSource
        dataSource.selectedIndexPath
            .bind(to: selectedIndexPath)
            .disposed(by: disposeBag)

        // observe viewModel
        viewModel.selectedRepository
            .bind(to: showRepository)
            .disposed(by: disposeBag)
        ...
    }
    
    private var showRepository: AnyObserver<GithubKit.Repository> {
        return UIBindingObserver(UIElement: self) { me, repository in
            let vc = RepositoryViewController(repository: repository,
                                              favoritesOutput: me.favoritesOutput,
                                              favoritesInput: me.favoritesInput)
            me.navigationController?.pushViewController(vc, animated: true)
        }.asObserver()
    }
    ...
}
```

上記のTableViewのCellが選択されてから、次のViewControllerに遷移するまでの流れは下図のようになります。

![MVVM.002.jpeg](https://qiita-image-store.s3.amazonaws.com/0/60325/425df32d-d269-999b-465e-74c9979fc79d.jpeg)

### お気に入りに追加

`favoriteButtonItem`のタップイベントを`favoriteButtonItem.rx.tap`として、`RepositoryViewModel`のinitializerに渡します。
そうすることによって、テスト時にPublishSubjectを利用することでタップを再現することができるようになります。

```swift:RepositoryViewController
final class RepositoryViewController: SFSafariViewController {
    private let favoriteButtonItem: UIBarButtonItem
    private let disposeBag = DisposeBag()
    private let viewModel: RepositoryViewModel

    init(repository: Repository,
         favoritesOutput: Observable<[GithubKit.Repository]>,
         favoritesInput: AnyObserver<[GithubKit.Repository]>) {
        let favoriteButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
        self.favoriteButtonItem = favoriteButtonItem
        self.viewModel = RepositoryViewModel(repository: repository,
                                             favoritesOutput: favoritesOutput,
                                             favoritesInput: favoritesInput,
                                             favoriteButtonTap: favoriteButtonItem.rx.tap)
        super.init(url: repository.url, entersReaderIfAvailable: true)
    }
    ...
}
```

`RepositoryViewModel`では、initializerの引数である`favoritesOutput: Observable<[GithubKit.Repository]>`からお気に入り一覧の配列の更新イベントを受け取ります。
その際に、ViewModelで保持している`GithubKit.Repository`が渡ってきた配列に含まれている場合は**配列と該当するindex**を返し、含まれていなかった場合は**配列とnil**を返します。
`favoriteButtonTap: ControlEvent<Void>`のイベントを受け取った際に上記の最新状態をwithLatestFromで受け取り、indexが存在した場合は**該当のindexを削除した状態の配列**返し、indexが存在しない場合は**ViewModelが保持している`GithubKit.Repository`を追加した配列**を返します。
そして、`favoritesInput: AnyObserver<[GithubKit.Repository]>`にその配列を流します。

```swift:RepositoryViewModel
final class RepositoryViewModel {
    ...
    private let disposeBag = DisposeBag()

    init(repository: Repository,
         favoritesOutput: Observable<[GithubKit.Repository]>,
         favoritesInput: AnyObserver<[GithubKit.Repository]>,
         favoriteButtonTap: ControlEvent<Void>) {
        let favoritesAndIndex = favoritesOutput
            .map { [repository] favorites in
                (favorites, favorites.index(where: { $0.url == repository.url }))
            }
        ...
        favoriteButtonTap
            .withLatestFrom(favoritesAndIndex)
            .map { [repository] favorites, index in
                var favorites = favorites
                if let index = index {
                    favorites.remove(at: index)
                    return favorites
                }
                favorites.append(repository)
                return favorites
            }
            .subscribe(onNext: { favoritesInput.onNext($0) })
            .disposed(by: disposeBag)
    }
}
```

`FavoriteViewModel`では、initializerの引数として受け取った`favoritesObservable: Observable<[GithubKit.Repository]>`を利用して外部からのイベントを受け取ります。
favoritesObservableを_favoritesにbindして、保持している配列を更新します。
_favoritesが更新されたイベントを`Observable<Void>`に変換して、`relaodData: Observable<Void>`として外部に公開します。

```swift:FavoriteViewModel
final class FavoriteViewModel {
    ...
    let relaodData: Observable<Void>
    ...
    private let _favorites = Variable<[GithubKit.Repository]>([])
    private let disposeBag = DisposeBag()

    init(favoritesObservable: Observable<[GithubKit.Repository]>, ...) {
        ...
        self.relaodData = _favorites.asObservable().map { _ in }
        ...
        favoritesObservable
            .bind(to: _favorites)
            .disposed(by: disposeBag)
    }
}
```

`FavoriteViewModel`で公開されているreloadDataを`FavoriteViewController`の`reloadData: AnyObserver<Void>`にbindすると`tableView.reloadData()`が実行されるので、`FavoriteViewController`上でリストが更新されます。


```swift:FavoriteViewController
final class FavoriteViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    ...
    private private(set) lazy var viewModel: FavoriteViewModel = {
        .init(favoritesObservable: self.favorites, ...)
    }()
    
    private let favorites = PublishSubject<[GithubKit.Repository]>()
    ...
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        // observe viewModel
        viewModel.relaodData
            .bind(to: reloadData)
            .disposed(by: disposeBag)
        ...
    }
    ...
    private var reloadData: AnyObserver<Void> {
        return UIBindingObserver(UIElement: self) { me, _ in
            me.tableView.reloadData()
        }.asObserver()
    }
}
```
お気に入りに追加/削除される流れは下記のようになります。

`FavoriteViewController`の`favoriteInput: Obsservable<[GithubKit.Repository]>`と`favoriteOutput: Observable<[GithubKit.Repository]>`は画面遷移時に、他のViewControllerのinitializerの引数として渡されていきます。
`RepositoryViewController`ではfavoriteOutputから受け取った配列をもとに、保持している`GithubKit.Repository`が含まれているかを確認し、追加または削除を行った配列をfavoriteOutputに対して流します。
favoriteInputは`FavoriteViewModel`内で保持されている`_favorites: Variable<[GithubKit.Repository]>`にbindし、その更新イベントをfavoriteOutputによって外部に公開されます。
また、_favoritesの更新イベントを`Observable<Void>`に変換し`reloadData`として公開することで、`FavoriteViewController`側でreloadData: AnyObserver<Void>`にbindされ、`tableView.reloadData()`が実行されるので`FavoriteViewController`上でリストが更新されます。

![MVVM.003.jpeg](https://qiita-image-store.s3.amazonaws.com/0/60325/9f3b49b5-6df1-2046-4a8d-fbeb1217089c.jpeg)

### テスト

`FavoriteViewModel`はinitializerの引数として、`favoritesObservable: Observable<[GithubKit.Repository]>`と`selectedIndexPath: Observable<IndexPath>`を受け取っています。
XCTestなどでテストを行う際に、それらを外部から注入してイベントを送ることで、ViewModelのプレゼンテーション層に関するロジックのテストが可能になります。
テストメソッド内では該当するsubscribeのclosureに対して期待する結果を記述し、その後に紐づくPublishSubjectからイベントを送っています。

```swift:FavoriteViewModelTests
class FavoriteViewModelTests: XCTestCase {
    
    var viewModel: FavoriteViewModel!
    var favorites: PublishSubject<[GithubKit.Repository]>!
    var selectedIndexPath: PublishSubject<IndexPath>!
    
    override func setUp() {
        super.setUp()
        
        self.viewModel = FavoriteViewModel(favoritesObservable: self.favorites,
                                           selectedIndexPath: self.selectedIndexPath)
    }
    
    func testRelaodData() {
        let expectation = self.expectation(description: "testReloadData expectation")
        
        let disposable = viewModel.relaodData
            .subscribe(onNext: {
                expectation.fulfill()
            })
        
        let repository = GithubKit.Repository.mock()
        favorites.onNext([repository])
        
        waitForExpectations(timeout: 1, handler: nil)
        disposable.dispose()
    }
    
    func testSelectedRepository() {
        let expectation = self.expectation(description: "testSelectedRepository expectation")
        
        let repository = Repository.mock()
        let disposable = viewModel.selectedRepository
            .subscribe(onNext: { repo in
                XCTAssertEqual(repository.url, repo.url)
                expectation.fulfill()
            })
        
        favorites.onNext([repository])
        
        waitForExpectations(timeout: 1, handler: nil)
        disposable.dispose()
    }
}
```

### 考察

ViewとViewModelはData bindingによってやり取りが行われるため、ViewはViewModelの参照を持ちますがViewModelは直接的にViewに依存はしません。
また、ViewModelのinputに対して擬似的にユーザーのアクションをイベントとして送り、ViewModelのoutputのイベントを確認することでプレゼンテーション層に関するロジックのテストが可能です。
そういった利点がある一方で、標準フレームワークだけでData bindingを実現することは容易ではありません。
NotificationCenterやKVOを利用することで近しいものは実装できますが、取得できる値の型がAnyになってしまうという問題があります。
また、propertyのdidSetでdelegateのメソッドを呼ぶことで近しい実装もできますが、delegateのオブジェクトは基本的に1つなので複数にしたい場合はforwardInvocationを利用する必要があったりしてしまいます。

## Flux

最後に、Fluxによる実装について書いていきます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/60325/802eac5d-8ec7-597d-ee92-e7370d074239.jpeg" width="50%">

Fluxのパーツとして

- **Action**...Viewからイベントを処理し、Dispatcherに値を流します。
- **Dispatcher**...Actionから受け取った値を、Storeに流します。
- **Store**...Dispatcherから受け取った値を保持し、値の変更を通知します。
- **View**...ユーザーかの操作によってActionを実行し、Storeの変更を反映します。

があります。
それらは、**View → Action → Dispather → Store → View**という形で単一方向のデータフローになっています。
ActionはどのViewからでも実行ができ、StoreはどのViewから参照されたとしても値を取得することができます。

このサンプルの場合、Fluxのパーツの構成は

- 1つのDispatcher
- ActionとStoreが一対一の組み合わせが複数

となっています。


また、ViewControllerごとにActionやStoreがあるわけではなく

- `GithubのRepositoryに関するActionとStore`
- `GithubのUserに関するActionとStore`

という作りになっています。

※Action、Dispatcher、Storeの実装を簡易化するために、[FluxCapacitor](https://github.com/marty-suzuki/FluxCapacitor)を利用しています。

### 画面遷移

`UITableViewDataSource`や`UITableViewDelegate`の実装を、`FavoriteViewController`の場合は`FavoriteViewDataSource`に移しています。
`FavoriteViewDataSource`内の`tableView(_:didSelectRowAt:)`では、`RepositoryStore`から該当の`GithubKit.Repository`を取得し、`RepositoryAction`の`selectRepository(_:)`に渡されています。

```swift:FavoriteViewDataSource
final class FavoriteViewDataSource: NSObject, UITableViewDelegate, ... {
    private let store: RepositoryStore
    private let action: RepositoryAction

    init(store: RepositoryStore = .instantiate(),
         action: RepositoryAction = .init()) {
        self.store = store
        self.action = action
    }
    ...
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let repository = store.favoritesValue[indexPath.row]
        action.selectRepository(repository)
    }
    ...
}
```

`RepositoryAction`は受け取った`GithubKit.Repository`を`Dispatcher`に渡し、`RepositoryStore`は`Dispatcher`から`GithubKit.Repository`を受け取ります。
受け取った`GithubKit.Repository`は`_selectedRepository: Variable<GithubKit.Repository?>`に保持されます。
`selectedRepository: Observable<GithubKit.Repository?>`が外部に公開されているので、値が更新された際にイベントが流れます。

```swift:RepositoryFluxFlow
final class RepositoryAction: Actionable {
    typealias DispatchValueType = Dispatcher.Repository
    ...
    func selectRepository(_ repository: GithubKit.Repository) {
        invoke(.selectedRepository(repository))
    }
    ...
}

extension Dispatcher {
    enum Repository: DispatchValue {
        ...
        case selectedRepository(GithubKit.Repository?)
        ...
    }
}

final class RepositoryStore: Storable {
    typealias DispatchValueType = Dispatcher.Repository
    ...
    let selectedRepository: Observable<GithubKit.Repository?>
    var selectedRepositoryValue: GithubKit.Repository? {
        return _selectedRepository.value
    }
    private let _selectedRepository = Variable<GithubKit.Repository?>(nil)
    ...
    init(dispatcher: Dispatcher) {
        ...
        self.selectedRepository = _selectedRepository.asObservable()
        ...
        register { [weak self] in
            switch $0 {
            ...
            case .selectedRepository(let value):
                self?._selectedRepository.value = value
            ...
            }
        }
    }
}
```

`FavoriteViewController`では、`RepositoryStore`の`selectedRepository: Observable<GithubKit.Repository?>`から流れてきたイベントを受け取り、そのイベントの値をVoidに変換して、`showRepository: AnyObserver<Void>`にbindします。
ViewControllerの遷移時、他のデザインパターンと異なる部分になるのですが、`RepositoryViewController`のinitializerは引数として`GithubKit.Repository`を渡していません。

※store.selectedRepositoryはviewDidAppear時にsubscribeされ、viewDidDisappear時にdisposeされる実装がされています。

```swift:FavoriteViewController
final class FavoriteViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!

    private let dataSource = FavoriteViewDataSource()
    private let disposeBag = DisposeBag()
    private let store: RepositoryStore = .instantiate()
    private let action = RepositoryAction()

    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        // observe store
        rx.methodInvoked(#selector(FavoriteViewController.viewDidAppear(_:)))
            .flatMapLatest { [weak self] _ -> Observable<Void> in
                guard let me = self else { return .empty() }
                let viewWillDisappear = me.rx
                    .sentMessage(#selector(FavoriteViewController.viewDidDisappear(_:)))
                return me.store.selectedRepository
                    .takeUntil(viewWillDisappear)
                    .filter { $0 != nil }
                    .map { _ in }
            }
            .bind(to: showRepository)
            .disposed(by: disposeBag)
        ...
    }
    
    private var showRepository: AnyObserver<Void> {
        return UIBindingObserver(UIElement: self) { me, _ in
            guard let vc = RepositoryViewController() else { return }
            me.navigationController?.pushViewController(vc, animated: true)
        }.asObserver()
    }
    ...
}
```

`GithubKit.Repository`を引数として渡す必要がない理由として、`FavoriteViewDataSource`内の`tableView(_:didSelectRowAt:)`で`RepositoryStore`の`_selectedRepository: Variable<GithubKit.Repository?>`に保持されています。
そのため、`store.selectedRepositoryValue`として値を取り出すことで、該当の`GithubKit.Repository`を受け取ることができます。
ViewController内で実際に利用する場合、`store.selectedRepository`を`Observable<GithubKit.Repository>`に変換して利用します。

```swift:RepositoryViewController
final class RepositoryViewController: SFSafariViewController {
    private let favoriteButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
    private let disposeBag = DisposeBag()
    private let action = RepositoryAction()
    private let store: RepositoryStore

    init?() {
        let store = RepositoryStore.instantiate()
        guard let repository = store.selectedRepositoryValue else { return nil }
        self.store = store
        super.init(url: repository.url, entersReaderIfAvailable: true)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        // observe store
        let repository = store.selectedRepository
            .filter { $0 != nil }
            .map { $0! }
        ...
    }
}
```

上記のTableViewのCellが選択されてから、次のViewControllerに遷移するまでの流れは下図のようになります。

![Flux.002.jpeg](https://qiita-image-store.s3.amazonaws.com/0/60325/748c883b-c721-9d9c-dcc4-5ee0ea9db62a.jpeg)

### お気に入りに追加

`store.selectedRepository`を`Observable<GithubKit.Repository>`に変換した`noNilRepository`を利用します。
`noNilRepository`と`store.favorites`をcombineLatestし、**favoritesに`GithubKit.Repository`が含まれているか**と**`GithubKit.Repository`**に変換したものを`containsRepository: Observable<(Bool, Repository)>`とします。
`favoriteButtonItem.rx.tap`のイベントが渡ってきたら、withLatestFromで`containsRepository`の最新状態を取得します。
そのイベントをsubscribeし、`GithubKit.Repository`が含まれていたら`action.removeFavorite(:_)`、含まれていなかったら`action.addFavorite(_:)`を実行します。

```swift:RepositoryViewController
final class RepositoryViewController: SFSafariViewController {
    private let favoriteButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
    private let disposeBag = DisposeBag()
    private let action: RepositoryAction
    private let store: RepositoryStore

    init?(action: RepositoryAction = .init(),
         store: RepositoryStore = .instantiate()) {
        guard let repository = store.selectedRepositoryValue else { return nil }
        self.action = action
        self.store = store
        super.init(url: repository.url, entersReaderIfAvailable: true)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        // observe store
        let noNilRepository = store.selectedRepository
            .filter { $0 != nil }
            .map { $0! }
        let containsRepository = Observable.combineLatest(noNilRepository, store.favorites)
            { repo, favs in (favs.contains { $0.url == repo.url }, repo) }

        favoriteButtonItem.rx.tap
            .withLatestFrom(containsRepository)
            .subscribe(onNext: { [weak self] contains, repository in
                if contains {
                    self?.action.removeFavorite(repository)
                } else {
                    self?.action.addFavorite(repository)
                }
            })
            .disposed(by: disposeBag)
        ...
    }
}
```

`RepositoryAction`の`addFavorite(_:)`または`removeFavorite(_:)`が呼ばれるとDispatcherを介し、`GithubKit.Repository`を`RepositoryStore`で受け取ります。
`RepositoryStore`では、`addFavorite`の場合はfavoritesに追加をし、`reomveFavorite`の場合はfavoritesから削除をします。
それらの更新イベントを、外部に公開されている`favorites: Observable<[GithubKit.Repository]>`に流します。

```swift:RepositoryFluxFlow
final class RepositoryAction: Actionable {
    typealias DispatchValueType = Dispatcher.Repository
    ...
    func addFavorite(_ repository: GithubKit.Repository) {
        invoke(.addFavorite(repository))
    }

    func removeFavorite(_ repository: GithubKit.Repository) {
        invoke(.removeFavorite(repository))
    }
    ...
}

extension Dispatcher {
    enum Repository: DispatchValue {
        ...
        case addFavorite(GithubKit.Repository)
        case removeFavorite(GithubKit.Repository)
        ...
    }
}

final class RepositoryStore: Storable {
    typealias DispatchValueType = Dispatcher.Repository
    ...
    let favorites: Observable<[GithubKit.Repository]>
    var favoritesValue: [GithubKit.Repository] {
        return _favorites.value
    }
    private let _favorites = Variable<[GithubKit.Repository]>([])
    ...
    init(dispatcher: Dispatcher) {
        ...
        self.favorites = _favorites.asObservable()
        self.selectedRepository = _selectedRepository.asObservable()
        ...
        register { [weak self] in
            switch $0 {
            ...
            case .addFavorite(let value):
                if self?._favorites.value.index(where: { $0.url == value.url }) == nil {
                    self?._favorites.value.append(value)
                }
            case .removeFavorite(let value):
                if let index = self?._favorites.value.index(where: { $0.url == value.url }) {
                    self?._favorites.value.remove(at: index)
                }
            ...
            }
        }
    }
}
```

`FavoriteViewController`では`RepositoryStore`の`favorites`のイベントを受け取ると、Voidに変換し`reloadData: AnyObserver<Void>`にbindします。
すると`tableView.reloadData()`を実行され、`FavoriteViewController`上でリストが更新されます。

```swift:FavoriteViewController
final class FavoriteViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView! 

    private let dataSource = FavoriteViewDataSource()
    private let disposeBag = DisposeBag()
    private let store: RepositoryStore = .instantiate()
    private let action = RepositoryAction()

    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        store.favorites.map { _ in }
            .bind(to: reloadData)
            .disposed(by: disposeBag)
    }
    ...
    private var reloadData: AnyObserver<Void> {
        return UIBindingObserver(UIElement: self) { me, _ in
            me.tableView.reloadData()
        }.asObserver()
    }
}
```

お気に入りに追加/削除される流れは下図のようになります。

`RepositoryStore`はどこからでも参照が可能なので、`FavoriteViewController`からの遷移時に`RepositoryViewController`のinitializerの引数として渡す必要がありません。
`RepositoryViewController`では、`RepositoryStore`の`favorites`に`RepositoryStore`の`selectedRepository`が含まれているかを確認し、`RepositoryAction`から追加または削除を実行します。
その結果が`RepositoryStore`に反映され、公開している`favorites: Observable<[GithubKit.Repositor]>`にイベントが流れます。
favoritesの更新イベントを`FavoriteViewController`内で`Observable<Void>`に変換し`reloadData: AnyObserver<Void>`にbindされ、`tableView.reloadData()`が実行されるので`FavoriteViewController`上でリストが更新されます。

![Flux.003.jpeg](https://qiita-image-store.s3.amazonaws.com/0/60325/8c0321c6-0414-d0f4-d9ac-1f18bf9dfd1b.jpeg)

### テスト

下記のようにXCTestなどで、`RepositoryAction`で渡した値が`RepositorStore`に反映されるのかのテストを実行することができます。
テストメソッド内では該当するStore内のObservableをsubscribeし、closureに対して期待する結果を記述します。
その後に紐づくActionのメソットを実行しています。

```swift
class RepositoryFluxFlowTestCase: XCTestCase {
    var action: RepositoryAction!
    var store: RepositoryStore!
    
    override func setUp() {
        super.setUp()

        self.action = RepositoryAction()
        self.store = RepositoryStore.instantiate()
    }

    func testSelectedRepository() {
        let expectation = self.expectation(description: "testSelectedRepository expectation")
        
        let repository = Repository.mock()
        let disposable = store.selectedRepository
            .skip(1)
            .subscribe(onNext: {
                guard let selectedRepository = $0 else {
                    XCTFail()
                    return
                }
                XCTAssertEqual(repository.url, selectedRepository.url)
                expectation.fulfill()
            })
        
        action.invoke(.selectedRepository(repository))
 
        waitForExpectations(timeout: 1, handler: nil)
        disposable.dispose()
    }
    ...
}
```

またActionではメソッドの引数として受け取った値をdispatchするだけではなく、通信を行ってその結果をdispatchする場合もあります。
その場合、Actionのinitializerで通信用のモジュールをprotocolとして抽象化したものを引数として受け取ることで、通信結果の値がStoreに反映されるかのテストをすることができるようになります。
下記の例の場合は、`ApiSessionType`として通信用のモジュールを抽象化し、`func send<T: Request>(_ request: T) -> Observable<Response<T.ResponseType>>`を宣言しています。

```swift
protocol ApiSessionType: class {
    func send<T: Request>(_ request: T) -> Observable<Response<T.ResponseType>> 
}

final class RepositoryAction: Actionable {
    typealias DispatchValueType = Dispatcher.Repository
    
    private let session: ApiSessionType
    private var disposeBag = DisposeBag()
    
    init(session: ApiSessionType = ApiSession.shared) {
        self.session = session
    }
    ...
    func fetchRepositories(withUserId id: String, after: String?) {
        let request = UserNodeRequest(id: id, after: after)
        session.send(request)
            .subscribe(onNext: { [weak self] in
                self?.invoke(.lastPageInfo($0.pageInfo))
                self?.invoke(.addRepositories($0.nodes))
                self?.invoke(.repositoryTotalCount($0.totalCount))
            })
            .disposed(by: disposeBag)
    }
    ...
}
```

`ApiSessionType`を採用している`ApiSessionMock`では、`result: ApiSession.Result<Any>?`として通信の擬似的な結果を外部から設定できるようになっています。
`ApiSessionMock`で`func send<T: Request>(_ request: T) -> Observable<Response<T.ResponseType>>`を実装する際に、resultのAssociated ValueをObservableにラップして返しています。

```swift
class ApiSessionMock: ApiSessionType {
    enum Error: Swift.Error {
        case requestFailed
    }
    
    var result: ApiSession.Result<Any>?
    
    func send<T: Request>(_ request: T) -> Observable<Response<T.ResponseType>> {
        switch result {
        case .success(let value as Response<T.ResponseType>)?:
            return .just(value)
        default:
            return .error(Error.requestFailed)
        }
    }
}
```

`RepositoryFluxFlowTestCase`で`RepositoryAction`を初期化する際に、引数として`ApiSessionMock`のインスタンスを渡します。
`testFetchRepositories()`では、まず通信の擬似的な結果をsession.resultに設定します。
そして、storeで保持している値から更新イベントを受け取った際に期待する結果をsubscribe内に記述します。
最後に、`action.fetchRepositories(withUserId:, after:)`を呼んでテストを行います。

```swift
class RepositoryFluxFlowTestCase: XCTestCase {
    var session: ApiSessionMock!
    var action: RepositoryAction!
    var store: RepositoryStore!
    
    override func setUp() {
        super.setUp()
        
        self.session = ApiSessionMock()
        self.action = RepositoryAction(session: self.session)
        self.store = RepositoryStore.instantiate()
    }
    
    func testFetchRepositories() {
        let expectation = self.expectation(description: "testFetchRepositories expectation")
        
        let repository = Repository.mock()
        let pageInfo = PageInfo.mock()
        let totalCount = 10
        let response = Response<Repository>(nodes: [repository], pageInfo: pageInfo, totalCount: totalCount)
        session.result = .success(response)
        let disposable =
            Observable.zip(store.repositories.skip(1),
                           store.lastPageInfo.skip(1),
                           store.repositoryTotalCount.skip(1))
                .subscribe(onNext: { repositories, lastPageInfo, repositoryTotalCount in
                    guard let firstRepository = repositories.first, let lastPageInfo = lastPageInfo else {
                        XCTFail()
                        return
                    }
                    XCTAssertEqual(repository.url, firstRepository.url)
                    XCTAssertEqual(lastPageInfo.hasNextPage, pageInfo.hasNextPage)
                    XCTAssertEqual(repositoryTotalCount, totalCount)
                    expectation.fulfill()
                })
        
        action.fetchRepositories(withUserId: "marty-suzuki", after: nil)
        
        waitForExpectations(timeout: 1, handler: nil)
        disposable.dispose()
    }
}
```

### 考察

Fluxではデータフローが単一方向になるので

- Actionではメソットの呼び出し
- Storeから値を参照

という形で責務を明確にすることができます。
そういった良い部分もある中で

- プレゼンテーション層に関するロジックがViewControllerに実装されがち
- Dispatcherはシングルトンで実装される（場合によってはStoreも）

という懸念点もあります。

# To Be Continued

番外編としてFluxとMVVMを組み合わせた実装に関しての資料を紹介だけさせていただいていました。
9/7に開催されたBrooklyn Swift Developersというmeetupでの登壇資料です。

<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Thank you for giving me a great opportunity to talk in an international meetup! <br>This is my talk slide! <a href="https://twitter.com/BKLNSwift">@BKLNSwift</a><a href="https://t.co/NFDWMBgbwT">https://t.co/NFDWMBgbwT</a></p>&mdash; marty-suzuki (@marty_suzuki) <a href="https://twitter.com/marty_suzuki/status/906011781050449921">September 8, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

またFluxとMVVMを組み合わせた実装のサンプルは下記のURLからご覧いただけます。
https://github.com/marty-suzuki/FluxCapacitor/tree/master/Examples/Flux%2BMVVM

## Flux & MVVM

Fluxではプレゼンテーション層に関するロジックがViewControllerに実装されがちですが、そこにViewModelを挟むことによって改善されます。
まず、`FavoriteViewController`でのFlux単体でのデータフローは下図のようになります。

![flux_mvvm.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/60325/88251db0-e50d-07bc-0854-40ac6b741dda.jpeg)

そこにViewModelを挟むと下図のようになります。

![flux_mvvm.002.jpeg](https://qiita-image-store.s3.amazonaws.com/0/60325/c219d100-18b7-bea0-bb9a-5ca08699c5c8.jpeg)

`FavoriteViewModel`では`RepositoryStore`で公開されているObservableを変換して、`reloadData: Observable<Void>`として外部に公開します。

```swift:FavoriteViewModel
final class FavoriteViewModel {
    ...
    private let store: RepositoryStore
    private let disposeBag = DisposeBag()
    
    let reloadData: Observable<Void>
    private let _reloadData = PublishSubject<Void>()
    let showRepository: Observable<Void>
    private let _showRepository = PublishSubject<Void>()
    ...
    init(...,
         store: RepositoryStore = .instantiate(),
         ...) {
        self.store = store
        ...
        self.reloadData = _reloadData
        ...
        store.favorites
            .map { _ in }
            .bind(to: _reloadData)
            .disposed(by: disposeBag)
    }
}
```

`FavoriteViewController`では`FavoriteViewModel`で公開されているreloadDataを`reloadData: AnyObserver<Void>`にbindし、`tableView.reloadData()`を実行します。

```swift:FavoriteViewController
final class FavoriteViewController: UIViewController {
    ...
    private let disposeBag = DisposeBag()
    ...
    private(set) lazy var viewModel: FavoriteViewModel = {
       ...
    }()
    ...
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        viewModel.reloadData
            .bind(to: reloadData)
            .disposed(by: disposeBag)
        ...
    }
    
    private var reloadData: AnyObserver<Void> {
        return UIBindingObserver(UIElement: self) { me, _ in
            me.tableView.reloadData()
        }.asObserver()
    }
    ...
}
```

プレゼンテーション層のロジックがViewModelに実装されているため、ViewControllerではViewModelの公開されているObservableが該当のViewにbindされる実装になります。
そのため、ViewControllerをTestCaseに置き換えることで、容易にテストするとこが可能になります。

![flux_mvvm.003.jpeg](https://qiita-image-store.s3.amazonaws.com/0/60325/be10a08e-4396-7e1a-1578-cb0cd5516e6c.jpeg)

`FavoriteViewModelTestCase`では、`FavoriteViewModel`のinitializerで`RepositoryAction`と`RepositoryStore`を渡しています。
`testReloadData()`では、`RepositoryAction`の`addFavorite(_:)`に`Repository`を渡すと`viewModel.reloadData`が実行されるかのテストを行っています。

```swift:FavoriteViewModelTestCase
class FavoriteViewModelTestCase: XCTestCase {
    
    var action: RepositoryAction!
    var store: RepositoryStore!
    ...
    var viewModel: FavoriteViewModel!
    ...
    
    override func setUp() {
        super.setUp()
        ...
        self.action = RepositoryAction(...)
        self.store = RepositoryStore.instantiate()
        self.viewModel = FavoriteViewModel(action: action, store: store, ...)
    }

    func testReloadData() {
        let expectation = self.expectation(description: "testReloadData expectation")
        
        let disposable = viewModel.reloadData
            .subscribe(onNext: {
                expectation.fulfill()
            })
        
        action.addFavorite(Repository.mock())
        
        waitForExpectations(timeout: 1, handler: nil)
        disposable.dispose()
    }
}
```

# 最後に

MVC、MVP、MVVM、Fluxを比較してきましたがどのデザインパターンが良いというわけではなく、__"開発しているサービスにとってどのデザインパターンが必要十分なのか"__が重要だと考えています。
その要因はとして、アプリの規模だったり、アプリの性質だったり、開発している人数だったりと様々あるかと思います。
また実際にプロジェクトに導入する際には、それぞれのデザインパターンの簡単なサンプルを実装してみないと良し悪しはわからず机上の空論にしかならなので、是非サンプルを作ってみてください。