主要な実装の決定事項
読了時間:約12分
はじめに
Prismアーキテクチャを実装する際、開発者はアプリケーションの構造、保守性、開発体験に大きな影響を与えるいくつかの重要な決定に直面します。これらの決定は、アーキテクチャがコードでどのように実現されるかを形作り、プロジェクトのライフサイクル全体にわたってチームのワークフローに影響を与えます。
このドキュメントでは、Prismアーキテクチャを採用する際に開発者が行う必要がある主要な実装の決定事項を概説しています。これらの決定ポイントを理解することで、コア原則とメリットを維持しながら、特定のプロジェクトニーズに合わせてアーキテクチャをカスタマイズすることができます。
アーキテクチャ境界の決定
プロジェクト構造アプローチ
最初の、そして最も影響力のある決定の一つは、プロジェクト構造の観点からPrismアーキテクチャをどのように物理的に構成するかということです:
-
レイヤーごとのSwiftパッケージ:各アーキテクチャ層を独立したパッケージとして厳密に分離
- 明示的な依存関係を通じて強力なアーキテクチャ強制を提供
- レイヤー間の明確な境界による集中的な作業を可能に
- 初期の複雑さとセットアップのオーバーヘッドが増加
- 大規模なチームとエンタープライズアプリケーションに最適
-
モノリシックプロジェクト構造:すべてのレイヤーを単一のプロジェクト構造内に配置
- よりシンプルなセットアップと管理
- レイヤー間のリファクタリングが容易
- 境界を維持するために開発者の規律に依存
- 小規模なチームとアプリケーションに最適
-
ハイブリッドアプローチ:
- 基盤パッケージ:コア層と共通層をパッケージとし、他のレイヤーはモノリシック
- 機能ベースのパッケージ:すべてのレイヤーを横断する縦のスライスとして機能を実装
- アーキテクチャの強制と開発の柔軟性のバランスを取る
- 中規模のアプリケーションとチームに適している
この決定は、チームの規模、プロジェクトの複雑さ、再利用性のニーズ、およびアーキテクチャ境界の強制の重要性に基づいて行うべきです:
考慮事項:
- チームの規模と組織
- プロジェクトの規模と複雑さ
- アーキテクチャ強制の優先度
- 開発速度の要件
- 再利用性のニーズ
レイヤーの可視性ルール
Prismアーキテクチャには、どのレイヤーが他のレイヤーにアクセスできるかに関する特定のルールがあります。これらのルールをどの程度厳密に強制するかを決定する必要があります:
-
コンパイラ強制境界:プロジェクト構造やアクセス修飾子を使用してレイヤーアクセスルールを強制
- 偶発的な違反を防止
- アーキテクチャ境界を明示的にする
- 開発の摩擦を増加させる可能性あり
-
規約ベースの境界:チームの規律とコードレビューに依存
- 進化するアーキテクチャにより柔軟
- セットアップの複雑さが低い
- 勤勉なレビュープロセスが必要
-
ハイブリッド強制:重要な境界はコンパイルで強制し、他は規約で強制
- 柔軟性とアーキテクチャの整合性のバランスを取る
- 最も重要な境界に強制を集中
Prismアーキテクチャの標準的なレイヤーアクセスルールは以下の通りです:
レイヤー間通信の決定
インテントとステート管理
Prismアーキテクチャにおける重要な決定の一つは、レイヤー間の通信に使用されるインテントオブジェクトとステートオブジェクトをどのように構造化するかということです:
- 共通層定義アプローチ(推奨):
- すべてのインテントとステートタイプを共通層で定義
- 目的と使用法ごとに明確なフォルダ構造で整理
- すべてのレイヤーがこれらの定義にアクセスできるようにする
- 実際のオブジェクトは実行時に発信元のレイヤーで作成
- レイヤー固有アプローチ(非推奨):
- 各レイヤーが独自の通信タイプを定義
- レイヤー間の依存関係の問題を引き起こす
- 通信フローの追跡が困難になる
- 一貫性のないパターンにつながる
推奨アプローチでは、共通層に明確なフォルダ構造を使用します:
Common/
├── Intent/
│ ├── Base/ # 基本インターフェース
│ ├── UI/ # UI固有のインテント
│ ├── Application/ # アプリケーションインテント
│ └── Data/ # データ操作インテント
└── State/
├── Base/ # 基本インターフェース
├── UI/ # UI固有のステート
├── Application/ # アプリケーションステート
└── Response/ # レスポンスステート
これにより、クリーンなレイヤー境界を維持しながら、一貫した通信パターンが確保されます。
通信フロー戦略
もう一つの重要な決定は、レイヤー間でデータと要求がどのように流れるかということです:
- パッケージ配送モデル(推奨):
- 通信オブジェクト(インテントとステート)は配送パッケージのように機能
- 共通からのテンプレートを使用して発信元のレイヤーで作成
- 目的地までアーキテクチャを通過
- 各レイヤーは必要なものだけを抽出
- オブジェクトは移動中も不変を維持
-
完全変換モデル:
- 各レイヤー境界で完全な変換が必要
- より高い分離性だが、変換のオーバーヘッドも高い
- より多くのマッピングコードを作成
-
混合フローモデル:
- 一部の境界は完全変換を使用
- 他はパッケージ配送を使用
- より複雑なメンタルモデル
推奨されるパッケージ配送モデルは、適切な境界を維持しながら効率性を創出します。
コア層の決定
エンティティの可変性
コア層のエンティティが以下のどれになるかを決定します:
-
不変エンティティ:不変値型としてのエンティティ(Swiftではstruct)
- 予期しない状態変更を防止
- デフォルトでスレッドセーフ
- 変更には新しいインスタンスが必要
- 関数型プログラミングの原則に従う
-
可変エンティティ:可変参照型としてのエンティティ(Swiftではclass)
- その場での修正を許可
- 複雑なオブジェクトにとってよりメモリ効率が良い
- 状態変更の慎重な管理が必要
- 複雑なオブジェクトグラフに適している
-
ハイブリッドアプローチ:制御された内部可変性を持つ不変パブリックインターフェース
- 外部に対して不変性を保証
- 効率的な内部操作を可能に
- より複雑な実装
この決定は、アプリケーション全体でのエンティティの取り扱い方と、採用するプログラミングパラダイムに影響します。
ID管理戦略
エンティティIDがどのように生成され管理されるかを決定します:
-
クライアント生成ID:クライアントアプリケーションでIDを生成
- オフライン操作を可能に
- 新しいエンティティの作成を簡素化
- 競合解決戦略が必要な場合がある
-
サーバー生成ID:バックエンドによって割り当てられるID
- システム内での一意性を保証
- サーバーサイドの実装を簡素化
- エンティティ作成のための追加のラウンドトリップが必要
-
ハイブリッドID戦略:エンティティタイプごとに異なる戦略
- エンティティタイプ間でニーズのバランスを取る
- 永続化されるまでの一時的なIDを許可する場合がある
この決定は、エンティティの作成、永続化、およびバックエンドシステムとの同期方法に影響します。
プロトコルvs具象型
コア層でプロトコルと具象型をいつ使用するかを決定します:
-
プロトコル重視アプローチ:ほとんどのコアコンポーネントにプロトコルを定義
- 柔軟性と置換可能性を最大化
- モックを使用したテストが容易
- 抽象化の複雑さを増加
-
具象型アプローチ:ほとんどのコンポーネントに具象型を使用
- 理解とナビゲーションを簡素化
- 抽象化のオーバーヘッドを削減
- 置換がより困難になる
-
戦略的プロトコル使用:置換が必要な場所でのみプロトコルを使用
- 抽象と具体性のバランスを取る
- 最も重要な場所での柔軟性に焦点を当てる
- 不必要な抽象化を削減
この決定は、コア層の柔軟性とテスト可能性に影響を与えます。
ドメイン層の決定
バリデーション戦略
ドメインバリデーションがどのように構造化され実装されるかを決定します:
-
エンティティ内部バリデーション:エンティティ内部のバリデーションロジック
- エンティティが常に有効であることを確保
- 妥当性を保証することで使用を簡素化
- エンティティの作成と更新が複雑になる可能性
-
独立バリデーションサービス:バリデーション専用のサービス
- エンティティをデータと基本的な振る舞いに集中させる
- コンテキスト依存のバリデーションを可能に
- コンテキスト間でバリデーションロジックの再利用を可能に
-
複合アプローチ:エンティティでの基本的なバリデーション、サービスでの複雑なバリデーション
- エンティティで基本的な整合性を確保
- 柔軟なコンテキスト依存のバリデーションを可能に
- 責任のバランスを取る
この決定は、ビジネスルールがどのように強制され、バリデーションロジックがアプリケーションのどこに存在するかに影響します。
ドメインイベント処理
ドメインイベントをどのように実装するかを決定します:
-
同期的ドメインイベント:すぐに処理されるイベント
- 実装とデバッグを簡素化
- 即時の一貫性を確保
- コンポーネント間に密結合を生む可能性
-
非同期ドメインイベント:後で処理するためにキューに入れられるイベント
- トリガーとなる操作のパフォーマンスが向上
- より複雑な実装
- 結果整合性の課題を引き起こす可能性
-
ハイブリッドイベント処理:重要なイベントは同期的に、その他は非同期的に処理
- 一貫性とパフォーマンスのバランスを取る
- 特定のニーズに基づいて最適化を可能に
- より複雑なイベント処理システム
この決定は、ドメインモデルが変更にどれだけ反応的かつ応答的であるかに影響します。
オーケストレーション層の決定
ユースケース入出力フォーマット
ユースケースの入力と出力の構造を決定します:
- インテント/ステートパターン(推奨):入力にインテント、出力にステートを使用
- 全体的な通信戦略に沿っている
- アプリケーション全体で一貫したパターンを作成
- 操作のトレーサビリティを向上
- 共通層の定義を使用
-
単純なパラメータリスト:直接のパラメータと戻り値
- コードのオーバーヘッドが少ない
- 複雑な操作ではあまり明確でない可能性
- パラメータ順序エラーがより発生しやすい
-
汎用コマンドパターン:ユースケースによって処理される汎用コマンドオブジェクト
- すべての操作の統一的な処理
- 元に戻すやコマンドログ記録をサポート
- より複雑な実装
推奨されるインテント/ステートパターンは、Prismアーキテクチャの全体的な通信アプローチに沿っています。
エラー処理戦略
オーケストレーション層でのエラーの処理と通信方法を決定します:
-
ステートベースのエラー通信:ステートオブジェクトにエラー情報を含める
- 全体的な通信パターンと一貫性がある
- エラー処理を明示的で予測可能にする
- エラーを通常のアプリケーションフローとして扱う
-
ドメイン固有のエラータイプ:ドメインエラー用のカスタムエラータイプ
- ドメイン概念を明確に伝える
- 正確なエラー処理を可能に
- エラータイプの増殖を増加
-
汎用エラーカテゴリ:詳細を含むより広範なエラーカテゴリ
- エラータイプの数を削減
- エラー処理コードを簡素化
- ドメイン固有性を一部失う可能性
-
結果型:throwsの代わりにResultなどの型を使用
- 関数シグネチャでエラー処理を明示
- より関数型のエラー処理を可能に
- 標準的なSwiftエラー処理とは異なる
この選択は、全体的な通信戦略とエラー処理の哲学に沿ったものである必要があります。
オーケストレーション階層の実装
階層的オーケストレーション構造をどのように実装するかを決定します:
-
厳格な階層: 明確に定義されたユースケース、オーケストレーションサービス、ワークフロー
- 階層構造の強力な強制
- 複雑さによる明確な組織化
- より単純な機能にとっては厳格すぎると感じる可能性
-
柔軟な階層: オーケストレーションレベル間のあまり厳格でない境界
- 様々な機能の複雑さに適応
- あまり一貫性のない組織化につながる可能性
- 開発者が最初に理解しやすい
-
段階的階層: シンプルに始め、必要に応じてより構造化されたものに進化
- 構造を現在の複雑さに合わせる
- 機能が成熟するにつれて自然な進化を可能に
- リファクタリングの規律が必要
この決定は、オーケストレーションコンポーネントがどのように相互作用し、時間とともにどのように進化するかを形作ります。
インフラストラクチャ層の決定
永続化技術
どの永続化技術を使用するかを決定します:
-
REST API: 伝統的なRESTエンドポイント
- 広くサポートされている
- より単純な実装
- 複雑なデータニーズには効率性が低い
-
GraphQL: スキーマベースの柔軟なクエリ
- 複雑な関連データに最適化
- オーバーフェッチとアンダーフェッチを削減
- より複雑なサーバー実装
-
ローカルストレージ: デバイス上の永続化
- オフライン操作を可能に
- より高速なデータアクセス
- 同期戦略が必要
-
ハイブリッドアプローチ: 複数の技術の組み合わせ
- 各アプローチの利点を最大化
- より複雑な実装
- 各ニーズに適した技術を活用
この決定は、データアクセスパターンと全体的なアプリケーションパフォーマンスに影響します。
リポジトリ実装戦略
リポジトリがどのように実装されるかを決定します:
-
伝統的なリポジトリ: 集約境界に焦点を当てる
- ドメインの整合性を保持
- 明確な責任
- 複雑なクエリには効率性が低い可能性
-
クエリサービス: 複雑なデータアクセスに最適化
- 複雑なデータニーズに対してより効率的
- 集約境界を越える可能性
- GraphQL最適化に適している
-
複合アプローチ: CRUDにはリポジトリ、読み取りにはクエリサービス
- 書き込みにはドメインの整合性を維持
- 複雑な読み取り操作を最適化
- 責任の明確な分離
この決定は、アプリケーションがデータをどれほど効率的に取得および管理できるかに影響します。
レスポンス作成アプローチ
データを返すインフラストラクチャコンポーネントを実装する際に、レスポンスをどのようにパッケージ化するかを決定します:
-
ステートオブジェクト返却(推奨): 共通層で定義されたステートオブジェクトを返す
- 全体的な通信パターンと一致
- 明確で予測可能な戻り値型
- 主要データとともにメタデータを含める能力
-
直接エンティティ返却: ドメインエンティティを直接返す
- より単純な実装
- 変換オーバーヘッドが少ない
- コンテキストデータを含める能力が限られる
-
カスタムレスポンスオブジェクト: 専用のレスポンス型を定義
- 特定のニーズに合わせて微調整
- 維持すべき型が増える
- アプリケーション全体での一貫性が低い
推奨されるアプローチは、全体的な通信戦略との一貫性を作ります。
プレゼンテーション層の決定
UIアーキテクチャパターン
プレゼンテーション層内で使用するUIアーキテクチャパターンを決定します:
- PrismUI: 推奨される階層的プレゼンターパターン
- Prismアーキテクチャ用に特別に設計
- 階層的プレゼンター構造
- インテントベースの通信
- 全体的なアーキテクチャと整合
-
MVVM: Model-View-ViewModelパターン
- 広く採用されている
- フレームワークのサポートが充実
- 双方向データバインディング機能
-
TCA/Redux: The Composable ArchitectureまたはReduxパターン
- 厳格な状態管理
- 予測性の高いデータフロー
- 強力な関数型プログラミングアプローチ
この選択は、チームの専門知識とアプリケーションの特定のニーズに沿ったものである必要があります。
状態管理アプローチ
UI状態をどのように管理するかを決定します:
-
不変状態オブジェクト: UI状態に不変オブジェクトを使用
- 予測可能な状態遷移
- デバッグが容易
- パフォーマンスのオーバーヘッドがある可能性
-
オブザーバブルプロパティ: リアクティブなプロパティ観測
- 動的更新
- フレームワークフレンドリー(SwiftUI、Combine)
- あまり追跡しやすくない状態変更を作成する可能性
-
複合アプローチ: オブザーバブルラッパーを持つ不変コア状態
- 予測可能性とリアクティブ性のバランスを取る
- 最新のUIフレームワークと連携
- より複雑な実装
この決定は、UIがどれだけ応答的で保守可能かに影響します。
インテント作成戦略
プレゼンテーション層でインテントオブジェクトを作成する際のアプローチを決定します:
-
コンポーネントレベル作成: UIコンポーネントが直接インテントを作成
- 発生源での即時インテント作成
- 階層を上に転送する必要がある場合あり
- より分散したインテント作成ロジック
-
プレゼンターレベル作成: プレゼンターがUIコールバックに基づいてインテントを作成
- より中央集中型のインテント作成
- UIとインテントロジック間のより明確な分離
- より多くのコールバックメソッドが必要な場合あり
-
ハイブリッド作成: インテントの複雑さに基づく戦略的配置
- シンプルなインテントはコンポーネントレベルで作成
- 複雑なインテントはプレゼンターレベルで作成
- 分散と中央集中のバランスを取る
この決定は、ユーザーアクションがアプリケーション操作にどのように変換されるかに影響します。
クロスカッティングの決定
依存性注入アプローチ
依存関係をどのように管理するかを決定します:
-
手動依存性注入: 依存関係を明示的に渡す
- シンプルな実装
- 明確な依存関係フロー
- 冗長になる可能性あり
-
サービスロケーター: 依存関係の中央レジストリ
- よりシンプルなコンポーネント初期化
- あまり明示的でない依存関係
- 依存関係を隠す可能性
-
依存性注入フレームワーク: 専用のDIフレームワークを使用
- 依存関係管理を自動化
- 複雑な依存関係グラフを処理可能
- フレームワークの複雑さを追加
この決定は、アプリケーション全体でコンポーネントがどのように作成され構成されるかに影響します。
並行処理モデル
どの並行処理アプローチを使用するかを決定します:
-
Swift Concurrency (async/await): 最新の構造化された並行処理
- 明確な非同期コードフロー
- 強力なコンパイラサポート
- 優れたエラー処理
-
Combineフレームワーク: リアクティブプログラミングアプローチ
- ストリームベースのデータ処理
- イベント駆動型アーキテクチャに適している
- より急な学習曲線
-
GCD (Grand Central Dispatch): 伝統的なキューベースの並行処理
- きめ細かいコントロール
- 古いコードに普及
- より複雑なエラー処理
この決定は、アプリケーション全体で非同期操作がどのように処理されるかを形作ります。
テスト戦略
アプリケーションをどのようにテストするかを決定します:
-
レイヤー分離テスト: レイヤーを独立してテストすることに焦点
- テスト用に関心事を分離
- より高速なテスト実行
- 統合の問題を見逃す可能性
-
フローベースのテスト: アーキテクチャを通じた完全なフローをテスト
- 統合の問題を捕捉
- 実際のユーザーシナリオをテスト
- テスト実行が遅い
-
複合テスト戦略: コンポーネントにはレイヤーテスト、統合にはフローテスト
- 分離と統合のバランスを取る
- 複数のレベルでのカバレッジ
- より包括的なテストスイート
この決定は、アプリケーションがどれだけ徹底的かつ効率的にテストできるかに影響します。
結論
このドキュメントで概説した実装の決定は、Prismアーキテクチャアプリケーションがどのように構造化され、開発され、維持されるかに大きな影響を与えます。すべてのプロジェクトに対して単一の「正しい」選択肢のセットはありませんが、これらの決定ポイントを理解することで、コア原則とメリットを維持しながら、特定の要件に基づいて情報に基づいた選択を行うことができます。
レイヤー間の通信については、推奨されるアプローチは以下の通りです:
- 明確なフォルダ構造を持つ共通層ですべてのインテントとステートタイプを定義する
- 実行時に発信元のレイヤーで実際のオブジェクトを作成する
- オブジェクトがアーキテクチャを流れるパッケージ配送モデルに従う
- すべての通信オブジェクトの不変性を維持する
このアプローチは、適切なレイヤー境界と関心事の分離を維持しながら、クリーンで一貫した通信メカニズムを提供します。
これらの決定を行う際には、以下を考慮してください:
- チームの規模と専門知識
- プロジェクトの規模と複雑さ
- 開発のタイムラインと優先事項
- 長期的な保守ニーズ
- プラットフォーム固有の考慮事項
- パフォーマンス要件
Prismアーキテクチャは適応可能に設計されていることを忘れないでください。これらの決定を特定のコンテキストに合わせることができますが、Prismアーキテクチャを効果的にするコア原則は維持します。