VIPERのキャッチアップ
システムアーキテクチャ
GUIアーキテクチャはプレゼンテーションとドメインを分けるものだった。しかしGUIアーキテクチャの文脈でのドメインとは「UIに関連しない処理全て」
そのためドメインの先でどのようにレイヤーを切り分けるか、システム全体をどのように接合すべきかは不明確
→ システムアーキテクチャの出番
レイヤードアーキテクチャ
Presentation -依存-> Application -依存-> Domain -依存-> Data
問題
-
Domain層がData層に依存する = ドメインのデータ構造が永続化のためのデータ構造に引きずられる。
そのためData層を別のものに置き換えたくなった時に困る(Domain層のテストにあたってDBの中身を書き換えて環境を整える必要があるなど) -
Data層がデータ構造を定義していることにより、Domain層に閉じ込めるべきドメイン知識がData層に漏出することがある
依存関係逆転の原則
Domain層は自身のために永続化機能の実装に影響されない形で「Data層が従うべきインターフェース」を定義する。そしてData層はそのインターフェースに合わせて実装する。
つまり、Data層は抽象化されたインターフェースに依存する。
Clean Architectureは何が「Clean」なのか
GUIアーキテクチャは具体的な環境に依存したViewを取り扱うものだったのに対して、システムアーキテクチャはその詳細を抽象化する方向に進化してきた。
ドメインがiOSアプリ・Androidアプリを問わず使うことができないならば、それは「フレームワークを前提」にしていて「Clean」とは言えない。
Cleanであれば変更に強く、再利用可能で、外側からテストできるドメインを実現できる。
レイヤー分割の副作用
- 上位(内側)のレイヤーでの一部の変更は依存する下位(外側)のレイヤーに影響する
- 「画面上に表示したい項目が増える」という改修がPresentation層から最奥のドメインに至る全てのレイヤーのフィールドに影響することも
- レイヤー間でデータ変換を行う場合、パフォーマンス低下の可能性
- 学習コストの増大
VIPER = Clean Architecture + MVP(Passive View) + Router
CleanArchitecture
- Entity : アプリケーションに依存しない、ドメインに関するデータ構造やビジネスロジック
- UseCase : アプリケーションで固有なロジック
- インターフェイスアダプタ:UseCase・フレームワークとドライバで使われるデータ構造を互いに変換する
- フレームワークとドライバ:データベース、Webなどのフレームワークやツールの「詳細」
VIPER
iOSアプリ設計パターン入門より
- View:画面表示とUIイベント受信。UIView(とUIViewController)。「フレームワークとドライバ」に相当
- Interactor:データ操作とユースケース。Entityを使ってビジネスロジックを表現。「UseCase」に相当
- Presenter:アーキテクチャ上のメディエータ 。UIKit非依存。「インターフェイスアダプター」に相当。
- Entity:Interactorによってのみ使われる単純な構造のモデル。「Entity」に相当。
- Router:画面遷移と新しい画面のセットアップ。
MVPの側面
- Presentation層とModel層の中間層がPresenterと名付けられている
- PresenterはViewへの参照をもち、直接更新する
Passive View:プレゼンテーションロジックを完全にPresenterに担当させる
内側にあるEntityやUseCaseは外側のWebAPIサーバやデバイスドライバなどに依存していないので、それらの完成を待つことなくロジックをテストできる
レイヤー間の相互関係のルール
内側の円は外側の円からのみ参照される。
→ 内側の円が外側の円を直接参照しては行けない。
= 内側のクラスが外側のクラスや関数を直接参照してはいけない
内側から外側への通信の実現
依存関係逆転の原則
入出力それぞれの仕様を満たすprotocolを内側に定義しておき、外側をそのprotocolに準拠させる。
そして内側のオブジェクトの参照を渡すことで内側のイベントを間接的に受け取れるようになる
依存関係逆転の原則を適用するレイヤー
全てのクラスが上記のようなprotocolを定義し、外側のレイヤーから参照を受け取るべきか? → No!
UseCaseとEntityの間は単純なコマンドとクエリにすべき。
なぜならアプリケーションに依存しないビジネスロジックであるEntityは、単純なコマンドとクエリだけ持てば成立するはず。そしてEntityが特定のUseCaseからだけを参照されることを期待する作りにしては行けない。
インターフェースアダプター(Presenter)とUseCaseの間か、フレームワーク(View)とドライバとの通信でのみ使うべき
Clean Architectureの各レイヤーを組み立てて円構造に仕立て上げるのは Mainコンポーネントと呼ばれる、アプリケーション起動時のセットアップを担うコンポーネントの役割。
このコンポーネントはフレームワークとドライバ層と同じ災害層に置かれ、起動後直ちにそれぞれのクラスをインスタンス化してアーキテクチャを組み上げる。
iOSアプリにおいては、ApplicationDelegateか、あるいはそこから呼ばれる専用オブジェクト。
Routerを導入するモチベーション
- 単一責任原則に則って、肥大化するViewControllerから画面の生成と繊維処理を実施する責務を引き剥がしたい
- 繊維先が多い画面で繊維処理を一箇所にまとめて簡潔に管理しやすくしたい
Routerの役割
- 繊維先のViewControllerが依存するコラボレーターの生成と注入
- 画面遷移の実施方法の定義
※UIKitにおける画面遷移処理はUIViewController上に実装されていて完全な切り離しは不可能