Details
-
GUIアーキテクチャ
-
システムアーキテクチャ
-
Presentation Domain Separation(PDS)
- ドメイン(Modelと呼ばれ、システム本来の関心領域)を、プレゼンテーション(Viewと呼ばれ、UIに関するロジック)から引き離す
- PDSを実践する際の具体的なレイヤー構造をパターンとして示すのが、GUIアーキテクチャ
- ウィジェット単位でプレゼンテーションロジックとドメインを分離する
- Modelの変更にたいし、オブザーバー 同期が行われる
- Model
- NotificationCenterを使用して、Modelの変更をViewとControllerに通知できるようにする
- didSetを使い、変数の変更を通知する
- Controller
- Modelに処理を依頼するため、Modelをプロパティとして保持する(だが、更新通知は受け取らない)
- Viewで定義するUIコンポーネントのイベントを直接受け取るために、
@objc
修飾子をつける- イベントを受け取ったら、Modelに処理を依頼する
- 実装時に必要になった知識
- 必須イニシャライザ
- サブクラスでの実装が必須となる
- Swiftとイニシャライザ
- メタタイプ(Type)
type(of:)
で型を取得でき、String.Type
のようになる- [Swift 3] 型名を取得する
- Swift Type Metadata (ja)
- 循環参照を起こさせない
[unowned self]
- キャプチャ
[]
で囲うとその後その変数に何を代入しても代入前の値を見るようになる - 普段なにげなく書いている[unowned self]の意味を調べる
- キャプチャ
- 必須イニシャライザ
- プレゼンテーションロジックの表現ができない
- プレゼンテーション状態を表現できない
- テストが難しい
- Presentation Modelに、プレゼンテーションロジックやプレゼンテーション状態の管理を担わせる
- MVCパターンの3つの課題を全て解決できる
- ロジックがViewから切り離されていれば、Viewのインスタンスを用意しなくてもテスト可能
- AspectAdaptorなどの仕組みの導入
- SwiftでいうKeyPathによるKVOのようなもので、Modelが更新された時の動作をプロパティの指定だけで簡単に描けるようにサポートしてくれている(よくわからん)
- 特になし?
- 構造は、Application Modelとほぼ変わらないが、Controllerのレイヤーが存在しない
- ViewのテンプレートをXAML(XMLベースのDSL)で宣言する
- その宣言に従い、システムが実行時に自動的にViewとViewModelをバインドするため、コードを書かずともViewModelの状態がViewへ反映される
- Viewクラスは動的に
- 関数型リアクティブプログラミング(Reactive Programming)と相性が良いので採用される
- 手続き的にではなく、宣言的にロジックを表現できる
- Model
- UIに関係しない純粋なドメインロジックやそのデータを持つ
- ViewやViewModelがなくてもビルド可能
- View
- ユーザー操作の受け付けと画面表示を担当する
- ViewModelが保持する状態とデータバインディングし、ユーザー入力に応じてViewModel自身が保持するデータを加工・更新する
- ViewModel
- View - Model間の画面表示のための仲介役
- 責務は次の通り
- Viewに表示するためのデータを保持
- Viewからイベントを受け取り、Modelの処理を呼び出す
- Viewからイベントを受け取り、加工して値を更新する
- Modelとは違い、画面上でどのように表示されるかのために、必要な状態とロジックを担う
- MVPのPresenterとの違い
- Preseterと違い、Viewに対しての手続的な処理が不要なので、Viewの参照を保持する必要がない
- 大きなアプリケーションのためのアーキテクチャであり、単純なアプリケーションに置いては実装コストが「オーバーキル」(過剰)である
- 大きなアプリケーションであったとしても、データバインディングがメモリ効率に影響を与える可能性がある
NotificationCenter
を使用して、MVVMアーキテクチャで実装する例はほとんどない- 使用する場合、これを使って送られたデータの型情報が失われてしまうのが、デメリット
- SwiftUIは、MVVMに相当すると考えられる
- 開発者から見て、アノテーションをつけることで自動的にViewModelの状態が自動的にViewへ反映される
- メモアプリ開発でSwiftUIによるMVVMを学んでみた
- MVVMでiOSアプリをつくってみた
- 以下3つのMVPが存在する
- MVP(Taligent)
- MVP(Supervising Controller)
- MVP(Passive View)
- MVCのそれぞれの責務をさらに細かく分割し、Controllerを一般化した存在として、Presenterを置いた
- Presenterは、アプリケーション全ての入力イベントを管理する
-
Viewの特徴
- MVCのViewとは異なり、ユーザー操作の受付をContollerではなく、Viewで行う
- Viewが直接Modelの変更を監視する
- オブザーバ同期
- フロー同期
-
Presenterの特徴
- Application Modelに似ているが、よりViewに近いレイヤー
- Application Modelは、Modelに属する
- Viewの実体を直接知っている
- 複数のViewのインスタンスを管理し、それらを階層的に取り扱う
- 1つのViewにつき、1つのPresenterを作成する(Passive Viewも共通)
- 仲介役なので、ViewとModelの双方を知っているが、どちらを依存関係の主にするかは特に定められてない。
- ただしiOSでは、ViewControllerが不可欠なので、「ViewがPresenterを知っている状態」とし、PresenterからはViewをweak参照で持つようにする
- Application Modelに似ているが、よりViewに近いレイヤー
-
Modelの特徴
- 純粋なドメインロジックの表現
-
MVP(Passive View)と異なる点
- 複雑なプレゼンテーションロジックは、Presenterが担当し、簡単なものはViewに残す
- ViewはControllerを介さずに直接Modelの更新を検知して、プレゼンテーションロジックを担当することもある
- テストしやすい、TDDサイクルを回せるアーキテクチャ
- Passive View
- オブザーバ同期をやめたView
- Viewが完全に受動的な存在に
- Appleが提唱する「Cocoa MVC」はMVP(Passive View)のことと考えて良い
- MVP(Supervising View)と異なる点
- プレゼンテーションロジックを完全に、Presenterに担当させる
- extensionの名付け方
- 基本的には「拡張したいクラス名 + カテゴリ名」
- 例えば
String+UTF8Data.swift
のように - 参考:Swift で Extension につけるファイル名のベストプラクティス
- 例えば
- 基本的には「拡張したいクラス名 + カテゴリ名」
- Protocolごとにextensionで分けて実装する
- 理由は以下の2つ
- 明示的に別処理であることを表すため
- 1つのソースファイルの肥大化を避けるため
- 参考:【Swift】Protocolごとにextensionで切り分けて実装するワケ
- 理由は以下の2つ
- 単一方向のデータフロー
- それによりどこで状態の変更が起きているか、わかりやすい
- 状態に関するコードは Store が持つので、ViewController の肥大化を防ぐことができる
- Action
- ActionCreator
- なんらかの処理を行い、Action を生成する
- 生成した Action を Dispatcher に送信する
- ActionCreator
- Dispatcher
- ActionCreator から Action を受け取る
- 受け取った Action を Store に伝える
protocol Dispatcher { // ActionCreator が Action を送信する func dispatch(_ action: Action) // Store から呼び出して、Callback を登録し、Action を受け取る func register(callback: () -> Void) }
- Store
- Dispatcher の
register
関数を使って Callback を登録し、その Callback から Action を受け取る - 受け取った Action の type と data をもとに、自身の状態を更新する
- 更新した場合は、View へ変更通知を送る
- 任意のコンテキストに対して、1つの Store が存在
- アプリケーションと同じライフサイクルか、シングルトンのクラスになりがち
- Dispatcher に Action を送る前に Store を生成しないと、Action に対する処理が実行されない場合があるので、注意
- Dispatcher の
- Fluxを発展させ、関数型言語のElmの影響を大きく受けたアーキテクチャ
同期方法 | 概要 | メリット | デメリット | 使用シーン |
---|---|---|---|---|
フロー同期 | 上位レイヤーのデータを下位レイヤーに都度セットしてデータを同期する、手続き的な同期方法。 | ・データのフローを追いやすい |
・手続き的なので、共通したデータを参照している全ての箇所の参照を持っておく必要がある。そのために参照の管理が煩雑になりやすい。 | push遷移でドリルダウンしていくような隣り合った画面間でのデータ共有 |
オブザーバ同期 | 関し元である下位レイヤーが関し先である上位レイヤーからObserverパターンで送られるイベント通知を受け取ってデータを同期させる、宣言的な同期方法。 | ・共通した複数箇所でデータを同期しやすい。 | ・データが変更される度に同期処理が実行されるため、いつデータが同期されるかが追いづらくなる | ・ハートや星マークでのお気に入りのデータ管理のように、複数タブや階層が離れた画面でも、各画面が共通のデータ領域の変更を関ししているために、同期箇所で他の画面の参照を持つ必要がない。 |
- 注意したいこと
- コンポーネント間の距離が遠いのに、無理にフロー同期を使用しないこと!
- 上位レイヤーが知らなくても良い下位レイヤーを知る(依存する)ことになり、データフローを追いやすいというメリットがなくなるため
- どちらが優れているとかではなく、それぞれメリットデメリットがあるので、設計段階でどちらを採用するか考える必要がある
- コンポーネント間の距離が遠いのに、無理にフロー同期を使用しないこと!
- 以下については未反映(随時更新予定)
- MVVM
- Flux
- Redux
- 選定基準
- 「全てのプレゼンテーションロジックをテスト可能にしたいか?」
- YES→Passive View
- NO→Supervising View
- アプリケーションで取り扱うプレゼンテーションロジックが簡単なものならば、テスト不要か?
- YES→Supervising View
- NO→Passive View
- 「全てのプレゼンテーションロジックをテスト可能にしたいか?」
- 迷った場合やMVPに慣れていない場合
- Passive Viewを選択!
- 「Viewが持つ簡単なプレゼンテーションロジック」の「簡単、」の判断が難しいため
- Passive Viewを選択!
- ドメインの先でどのようにレイヤーを切り分けるべきか、システム全体をどのように接合すべきかを示す仕組みのこと
- 関心によってレイヤーを分ける
- Domain層がData層に依存しないよう、Dependency Injection(依存オブジェクトの注入)を行った
- それにより、Data層へのドメイン知識の漏洩は防がれる
-
Presentation層でも、Domain層にあるべきドメイン知識が漏洩しがちだったが、それを防ぐ
- DataやPresentation層を「外部との接続に関するレイヤー」として捉える
-
ポートとアダプターの概念がある
- ポート
- 外部との接続
- 目的の単位で抽象化される
- アダプター
- 差し込まれた外部モジュールの実装詳細を隠蔽し、ポートが期待するインターフェースへの変換する
- プライマリアダプター
- アプリケーションを駆動するためのもの
- セカンダリアダプター
- アプリケーションによって駆動されるためのもの
- ポート
- 基本的な考えは、Hexagonal Architectureと変わりない
- 同じく、Data層を外側におき、Adapterパターンによってアプリケーションを疎結合に保つもの
- Hexagonal Architectureとの違い
- アプリケーションの視覚表現として、六角形ではなく円を用いている
- アプリケーション内部を複数の円で分割している
- 円の中心にあるのは、モデルの状態と振る舞いを表現するDomain Model
- Domain ServiceやApplication Serviceがあるが、必ずしもそのレイヤーに分かれている必要はない
- 4つの特徴
- アプリケーションは自立したオブジェクトモデルを取り囲むように作られる
- 内側のレイヤーはインターフェースを定義し、外側のレイヤーはそれを実装する
- 依存の方向は外側から内側
- Application Coreはインフラストラクチャ(Data層)抜きでコンパイル・実行できる
- 今まで提唱された様々なアーキテクチャを統合したもの
- 画面遷移をどう設計するか
- アプリのルートに存在するApplicaion Coordinatorを頂点とした階層構造によって画面遷移を表現する
- 一般的には、画面遷移を行う場合は、View Controller内で次のView Controllerをインスタンス化し、Navigation ControllerへのpushやModalのpresentを行うが、それだと、View Controllerは次のView Controllerを指定いる必要がある。
- 遷移先が複数存在する場合や、使いまわしたい場合に、遷移ロジックが肥大化するのを防ぐ
- 「View」「Iteractor」「Presenter」「Entity」「Routing」
- Clean Architecture + MVP(Passive View) + Router
- RouterはPresenterに画面遷移を指示する
- Routerの役割
- 遷移先のView Controllerの生成
- 遷移先のView Controllerが依存するコラボレーターの生成と代入
- 画面遷移の実施方法の定義