まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて

  • 1051
    いいね
  • 8
    コメント

<この記事は「Money Forward Advent Calendar 2015」の22日目の記事です>

この記事は、iOS Clean Architectureと実際にコードへ適用した内容について紹介します。
コードについては、改善の余地があるため随時修正していくと思います。
→ github: https://github.com/koutalou/iOS-CleanArchitecture

iOS開発においてよくある問題点

  • 「ビジネスロジックはModelに置くべき」と言うが、開発者によって理解や意見がバラバラで統一的な実装ができない
  • 度重なる仕様変更や複雑な仕様に対応するためにViewControllerや特定のModelが肥大化し、ビジネスロジックの本質を見失う
  • MVC,MVP,MVVMだけで考えると、どこかのレイヤが複数の責務を持つことになり依存度の高い複雑なコードが生まれてしまう

→ 上記を解決すべくiOS Clean Architectureについて紹介します

Clean Architectureについて

Clean Architectureとは、一言で言うとドメイン駆動開発(DDD)やユースケース駆動開発(UCDD)を意識して、ビジネスロジックをUIやFrameworkから引き離し、それぞれの層毎に役割と責任を分離したArchitectureになります
http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html

CleanArchitecture.jpg

Clean Architectureを使うことのメリット

  • ビジネスロジックが明確になる
    • 特定のClassにロジックが盛り込まれFatになり、他の役割との依存性が高くなる事態を防ぐ
  • フレームワークに依存しない
    • Clean Architectureは特定のフレームワークやライブラリに依存しないです
  • UIに依存しない
    • ビジネスロジックを変えずにUIのみ変更することができる
  • Data Storeに依存しない
    • データ連携先がサーバでも、端末内のDBでもビジネスロジックに影響が無い
    • ビジネスロジックは、データの保存先や取得先を知らないくて良い
  • テストの容易性
    • 全ての層でテストが導入しやすい

Clean Architectureを使うことのデメリット

  • コード量が多い
    • 役割毎に層が多くなるため、必然的にコード量が多くなります
    • プロトタイプや短期開発アプリケーションには向かないと思います

他のArchitectureとの比較

MVC architecture

mvc.png

MVP(MVVM) architecture

mvp_mvvm.png

DDD architecture

ddd.png

iOS Clean Architecture

  • 構成としては、DDD(ドメイン駆動開発)のDomain層, Data層をもう少し細かくレイヤを分けたようなイメージです

clean_architecture.png

各レイヤの役割と説明

Presentation layer

  • UIの表示やイベントのハンドリングを行います、ビジネスロジック処理はしません

View (ViewController)

  • 画面表示やユーザのタッチイベントなどのイベントをPresenterに通知する
  • Presenterから受けたModelのデータやステータスによりViewの表示を切り替える

Presenter

  • Viewからイベントを受け取り、必要があればイベントに応じたUseCaseを実行する
  • UseCaseから受け取ったデータをViewへ渡す
  • Viewがどうなっているか知らない

Domain layer

  • iOSアプリケーション依存のビジネスロジックはこのレイヤが担当します

UseCase

  • ユースケースに必要なロジック処理を記述する
    • どのデータをどのように取得するかここで実装する
  • UIには直接関与しない(View,ViewControllerから直接参照されない)

Translater

  • UseCaseで取得したEntityをPresentation層で使用するModelへ変換する
    • Viewで使用するために最適化したModelの作成を行う

Repository

  • UseCaseで取得したいデータのCRUD相当のI/Fを記述する
    • データ取得に必要なDataStoreへデータ処理のリクエストを行う
  • Repositoryは、データを扱うI/Fを定義するがどうやってデータを扱うか知らない
  • Domain層として記述しましたが、実態としてはDomain層とData層のI/Fです

Data layer

  • 通信やデータ管理のロジックはこのレイヤが担当します

DataStore

  • データを実際に取得更新する処理を記述する
  • サーバからデータを取得するか、DBやキャッシュのデータを使用するかどうかもここで判断する
    • iOSでは、API通信、Realm、CoreData等を扱う実装に相当
  • 複数のDataStoreを扱う場合はFactoryパターンを用いてRepositoryがData種別を意識しない設計にする

Entity

  • DataStoreで扱うことができるデータの静的なモデル
  • Entity自身を直接操作することはせず、Value objectとして使用する
  • EntityはPresentation層では使用されない

Sampleアプリケーション

  • 上記で一通り説明はしましたが、実際にコードを見た方が理解は早いと思うので、サンプルを作成しました
    • Twitterアカウントと連動してTimelineを表示するサンプルアプリケーションです

→ github: https://github.com/koutalou/iOS-CleanArchitecture

サンプルアプリケーションの特徴

低依存性

  • Presenterから見たViewとUseCaseのI/F
    • 各レイヤが依存しないようにViewとUseCaseでは以下のようにprotocolでI/Fを定義しており、Presenterが最低限必要なI/F以外参照しなくて良い構成となっています
ICATTimelineViewController.swift
protocol ICATTimelineViewInput: class {
    func setTimelinesModel(_: ICATTimelinesModel) -> Void
    func changedStatus(_: ICATTimelineStatus) -> Void
}
ICATTimelineUseCase.swift
protocol ICATTimelineUseCaseOutput: class {
    func loadTimelines(timelinesModel: ICATTimelinesModel)
    func notAuthorizedOrNoAccount()
    func loadTimelinesError(error: ICATError)
}

ビジネスロジックの明分化

  • このiOS Clean Architectureでは以下のようにビジネスロジックを明分化します
    • UseCase: アプリケーションに依存する処理のビジネスロジック
    • Translater: Viewに最適なModelの変換ロジック
    • DataStore: 通信したり永続化したデータを扱うロジック
  • これにより、上記3つのようなロジックが混合してロジックが複雑になるようなケースを防ぐことができます

RepositoryのTask化

  • RepositoryをSwiftTaskのTaskでラップすることで、UseCaseのビジネスロジックを容易に実装できるようにしています
  • TDDの観点からもテストを容易にするために良さそうです
  • RepositoryをSwiftTaskでラップすると決めた場合、他の全てのRepositoryもSwiftTaskでTask化する必要があります

RepositoryのSwiftTask I/F実装

ICATSocialAccountRepository.swift
lazy var dataStore: ICATSocialAccountDataStore = ICATSocialAccountDataStore()
func getTwitterAccountsTask() -> Task<Void, Array<ACAccount>?, ICATError> {
    return Task { _,fulfill, reject, configure in
        self.dataStore.getTwitterAccounts({ (accounts, error) -> Void in
            if (error.isError) {
                reject(error)
                return
            }

            fulfill(accounts)
        })
    }
}

UseCaseのTask処理実装

ICATLoginAccountUseCase.swift
weak var output: ICATLoginAccountUseCaseOutput?
lazy var loginAccountRepository: ICATLoginAccountRepository = ICATLoginAccountRepository()
lazy var socialAccountRepository: ICATSocialAccountRepository = ICATSocialAccountRepository()
var twitterAccountIdentifier: String?        

// このケースではSwiftTaskで同期処理
loginAccountRepository.getSelectedTwitterAccountTask().success { (identifier) -> Task<Void, Array<ACAccount>?, ICATError> in
    twitterAccountIdentifier = identifier
    return self.socialAccountRepository.getTwitterAccountsTask()
}
.success { (accounts) -> Void in
    self.acAccounts = accounts!
    let registeredAccountsModel = ICATRegisteredAccountTranslater.generateRegisteredAccount(accounts!, selectedIdentifier: twitterAccountIdentifier)
    self.output?.loadTwitterAccounts(registeredAccountsModel)
}

実装内容の説明

  • サンプルだけ見ても分からないかもしれないので、Timelineを表示する画面に絞って説明します

View

ICATTimelineViewController.swift

役割

  • データがあればTimelineを表示する
  • ステータスを教えてもらえれば適切な表示を行う

実装

  • Presenterへ画面表示のタイミング(イベント)を通知する
    • この画面ではタッチイベントなど他のイベントはなし
  • PresenterからViewへ通知してほしい情報をprotocolで実装する
  • PresenterからModelやステータスを受け取り表示を変える

特徴

  • 実際のコードを見るとわかると思いますが、最低限のイベント処理とModelやステータスに合わせたViewの更新しか実装がないため、ViewやViewControllerはかなりシンプルになります

Presenter

ICATTimelinePresenter.swift

役割

  • ViewとUseCaseのイベントとデータの橋渡しをする

実装

  • Viewから表示イベントを受け取り、Timelineデータ取得のUseCase(ICATTimelineUseCase)を実行する
  • UseCaseから受け取ったデータをTranslaterへ渡して整形する
    • データ整形(Entity -> Model)後にViewへ渡す
    • データの状態やエラーによりViewにステータス情報を渡す

特徴

  • Entity, Model変換はUseCase/Translaterが担当するためデータの修正は必要なく、イベントやデータの受け渡しとステータスの管理しかしないため、実装内容はかなりシンプルになります

UseCase

ICATTimelineUseCase.swift

役割

  • タイムラインデータ取得に必要な処理(実際に行うのは下位レイヤ)を行う
    • 同期的に行うのか非同期で行うのか、要求に合わせて実装する

実装

  • Twitterアカウントを取得して、Timelineデータを取得する処理を行う

特徴

  • 今回RepositoryのI/Fを全てSwiftTaskで実装してあり、UseCaseではTaskとしてデータを扱う処理のロジックを記述することができる
  • TDDも意識しており、Repositoryのテストを行うことも容易にできるようになります

Translater

ICATTimelineTranslater.swift

役割

  • EntityをViewの表示に最適なModelへ変換する

実装

  • Entity -> Model変換

Repository

ICATTimelineRepository.swift

役割

  • UseCaseから依頼されたTimelineデータ取得を行う

実装

  • DataStoreへデータ取得をリクエストする
  • UseCaseからの制御をTask化できるようにI/FをSwiftTaskでラップ

特徴

  • I/FをSwiftTaskでラップしているため、UseCaseのビジネスロジックが複雑でも容易に実装できる

DataStore

ICATTimelineDataStore.swift

役割

  • サーバまたはRealmからデータを取得してRepositoryに返す

実装

  • Realmにデータがあるか確認しあればデータをRepositoryに渡す
  • サーバにリクエストして、帰ってきたデータをRepositoryに渡してRealmへ更新する

特徴

実際にデータを取得したり更新したりする処理を一元管理できる

その他

DIについて

Clean ArchitectureやDDDにはDI(Dependency Injection)がありますが今回はあまり触れていません
本来のClean ArchitectureはDIを前提としており、よりメリットを享受するためには更に依存性を少なくする必要があります
コード的には、1つ上位のレイヤが1つ下のレイヤに依存することを決めていますが、Routing classを新たに設けて、Routingがそれぞれの依存関係を繋ぎ合わせるような実装にして各レイヤ間を疎結合にするイメージです

ただ、DIを考慮していない現時点でも既に導入障壁が高いため今回はここまでにしたいと思います。

敷居が高い

上記で少し述べましたが、導入時の敷居が高く慣れるまで大変だと思います
簡単なアプリケーションや継続的に開発しないアプリケーションでは必要ないかもしれないです

テストの容易性

何度か述べていますが、役割によりレイヤを明確に分けているためレイヤ毎のテストが可能です
長期的な開発・想定していない仕様に対応していくサービスほど、テストがより重要になってくるかと思います

Modelに対する理解

ここまで読んでいれば、「ビジネスロジックはModelに置く」ということがどれだけ曖昧な言葉かわかると思います
Modelと言っても、Clean ArchitectureではDomain/Data層に分かれ、更にUseCase/Translater/Repository/DataStore/Entityに分かれます

世間一般の「Model」は「Model層」と呼ぶ方が正しく、今後の開発でModelに対する理解がもっと広まれば良いなと思います、ビジネスロジックも同様ですね

まとめ

ここまでiOS Clean Architectureについて説明しましたが、必ずしもClean Architectureを導入することが正ではなく、考え方やメリット/デメリットを理解した上で状況に合わせてより良い開発手法で開発していくことが重要だと考えています

この記事がその手助けとなれば嬉しいです

参考文献

The Clean Architecture
The Clean Architecture | 8th Light

Android Clean Architecture
AndroidオールスターズでClean Architectureについて発表してきた&参考リンク集 - tomoima525's blog

MVC, MVP, MVVM関連
Webアプリケーション開発者から見た、MVCとMVP、そしてMVVMの違い - Qiita
MVVMのModelにまつわる誤解 - the sea of fertility
MVVMをベースに複雑な振る舞いをしっかり把握できるアプリ開発 - Qiita

VIPER, DDD, UCDD関連
Meet VIPER: Mutual Mobile's application of Clean Architecture for iOS apps - Mutual Mobile
AndroidではMVCよりMVPの方がいいかもしれない - Konifar's WIP