メインコンテンツまでスキップ

プロジェクト構造

読了時間:約15分

はじめに

Prismアーキテクチャプロジェクトの物理的な構造をどのように整理するかは、開発ワークフロー、チームコラボレーション、保守性、およびアーキテクチャの強制に大きな影響を与えます。Prismアーキテクチャの概念的なレイヤーは関心事の明確な分離を定義していますが、これらのレイヤーをコードベースでどのように配置するかを決定する必要があります。

このドキュメントでは、厳格なパッケージベースの実装からモノリシックなアプローチ、そしてハイブリッドソリューションまで、Prismアーキテクチャプロジェクトを構造化するさまざまなアプローチについて探ります。これらのオプションを理解することで、特定のプロジェクト要件とチームダイナミクスに最も適した構造を選択するのに役立ちます。

プロジェクト構造に関する主要な考慮事項

特定の構造的アプローチを探る前に、決定に影響を与えるべき以下の主要な要素を考慮してください:

アーキテクチャの強制

アーキテクチャの境界をどの程度厳格に強制したいですか?異なるプロジェクト構造はさまざまなレベルの強制を提供します:

  • コンパイラによる強制:モジュール/パッケージの依存関係を通じてコンパイラによって強制される境界
  • 規約ベース:チームの規律とコードレビューを通じて維持される境界
  • ハイブリッドアプローチ:重要な境界はコンパイル時に強制し、その他は規約によって強制

チーム組織

チームの規模、構造、および作業パターンがプロジェクト構造に影響を与える必要があります:

  • チーム規模:大規模なチームはより明示的な境界から恩恵を受けることが多い
  • チーム組織:機能チーム対プラットフォームチームでは構造的ニーズが異なる
  • 開発経験:より経験豊富なチームはあまり構造的な強制を必要としない場合がある

再利用性の要件

コードベースのどの部分が再利用される可能性があるかを考慮します:

  • プロジェクト間再利用:コア層と共通層は別々のモジュールにすることでしばしば恩恵を受ける
  • コンポーネントの再利用性:機能モジュールは異なるアプリ間で再利用される可能性がある
  • 内部再利用:開発者が既存のコンポーネントを見つけて再利用する容易さ

開発効率

日常の開発タスクに対する構造の影響:

  • ビルド時間:よりモジュール化された構造はインクリメンタルビルド時間を改善できる
  • ナビゲーションと発見:開発者が関連するコードをどれだけ容易に見つけられるか
  • リファクタリングのオーバーヘッド:アーキテクチャ境界間でコードを移動するコスト

アプローチ1:レイヤーごとのSwiftパッケージ

このアプローチでは、各アーキテクチャレイヤーを個別のSwiftパッケージとして実装し、レイヤー間に強力な境界を作成します。

構造

PrismProject/
├── CorePackage/ # コア層
│ ├── Sources/
│ └── Tests/ # コアコンポーネントのテスト+モック
├── CommonPackage/ # 共通層
│ ├── Sources/
│ └── Tests/ # 共通コンポーネントのテスト+モック
├── DomainPackage/ # ドメイン層
│ ├── Sources/
│ ├── Tests/ # ドメインコンポーネントのテスト+モック
│ └── Package.swift # CorePackageに依存
├── InfrastructurePackage/ # インフラストラクチャ層
│ ├── Sources/
│ ├── Tests/ # インフラストラクチャのテスト+モック
│ └── Package.swift # CorePackage, CommonPackageに依存
├── OrchestrationPackage/ # オーケストレーション層
│ ├── Sources/
│ ├── Tests/ # オーケストレーションのテスト+モック
│ └── Package.swift # Core, Domain, Infrastructureに依存
├── PresentationPackage/ # プレゼンテーション層
│ ├── Sources/
│ ├── Tests/ # プレゼンテーションのテスト+モック
│ └── Package.swift # Core, Common, Orchestrationに依存
└── App/ # メインアプリケーション
├── Sources/
└── Package.swift # 他のすべてのパッケージに依存

利点

  1. 明示的なアーキテクチャ強制:レイヤーの依存関係はパッケージ依存関係を通じてコンパイルレベルで強制される
  2. 明確な境界:レイヤーの責任を明示的にし、レイヤー間の不正アクセスを防止する
  3. 分離されたテスト:各レイヤーはレイヤー固有のモックを持つ専用のテストターゲットを持つ
  4. コンパイル効率:あるレイヤーの変更がコードベース全体の再コンパイルを強制しない
  5. チームスケーラビリティ:異なるチームが最小限の競合で異なるパッケージで作業できる
  6. 再利用性:コアパッケージと共通パッケージはプロジェクト間で簡単に再利用できる
  7. 焦点を絞ったスコープ:レイヤーで作業している開発者はそのレイヤーに関連するコードのみを見る

欠点

  1. セットアップの複雑さ:より多くの初期設定とパッケージ依存関係の管理が必要
  2. リファクタリングのオーバーヘッド:レイヤー間で機能を移動するにはパッケージの変更が必要
  3. ビルドシステムの複雑さ:より複雑なビルドパイプラインと依存関係解決の問題の可能性
  4. 断片化されたコードベース:アプリケーションの全体的な視点を得ることが難しくなる可能性
  5. 小規模チームへのオーバーヘッド:小規模なプロジェクトには不必要な複雑さをもたらす可能性

使用するタイミング

  • 大規模なエンタープライズアプリケーション
  • 複数のチームが並行して作業するプロジェクト
  • アーキテクチャの整合性が重要なコードベース
  • レイヤーが複数のアプリケーション間で再利用される可能性があるプロジェクト
  • Swiftパッケージの専門知識を持つチーム

アプローチ2:モノリシックプロジェクト構造

このアプローチでは、すべてのアーキテクチャレイヤーを単一のプロジェクト構造内に維持し、フォルダ構成と開発者の規律に依存します。

構造

PrismProject/
├── Sources/
│ ├── Core/ # コア層
│ ├── Common/ # 共通層
│ ├── Domain/ # ドメイン層
│ ├── Infrastructure/ # インフラストラクチャ層
│ ├── Orchestration/ # オーケストレーション層
│ └── Presentation/ # プレゼンテーション層
└── Tests/
├── CoreTests/
├── CommonTests/
├── DomainTests/
├── InfrastructureTests/
├── OrchestrationTests/
└── PresentationTests/

利点

  1. シンプルさ:標準的なプロジェクト構造でより簡単なセットアップと管理
  2. 柔軟性:レイヤー間でコードをリファクタリングして移動するのが容易
  3. 統一されたビルドプロセス:依存関係解決の問題が少ないより単純なビルドパイプライン
  4. 全体的な視点:アプリケーション全体を一度に理解するのが容易
  5. 低いオーバーヘッド:設定と管理が少なく済む
  6. 容易なナビゲーション:IDE内でレイヤー間を移動するのが簡単

欠点

  1. 限定的なアーキテクチャ強制:レイヤー境界を維持するために開発者の規律とコードレビューに依存する
  2. 偶発的な依存関係:レイヤー間の不適切な依存関係を偶発的に作成しやすい
  3. より大きなコンパイル単位:あるレイヤーの変更がより大きな再コンパイルをトリガーする可能性
  4. テスト汚染:テストがアクセスすべきでないコンポーネントにアクセスする可能性
  5. チーム競合:複数の開発者が同じレイヤーで作業する際にマージ競合が発生する可能性が高い

使用するタイミング

  • 小規模なアプリケーション
  • 小規模な開発チーム(1-3人の開発者)を持つプロジェクト
  • 開発の速度が優先されるプロトタイプとMVP
  • アーキテクチャがまだ進化しているプロジェクト
  • 構造より前に概念に集中したいPrismアーキテクチャを初めて使用するチーム

アプローチ3:ハイブリッド - 基盤パッケージ

このアプローチでは、コア層と共通層のみを別々のパッケージとして抽出し、アプリケーション固有のレイヤーをモノリシック構造に保持します。

構造

PrismProject/
├── CorePackage/ # パッケージとしてのコア層
│ ├── Sources/
│ └── Tests/
├── CommonPackage/ # パッケージとしての共通層
│ ├── Sources/
│ └── Tests/
└── MainApp/ # メインアプリケーション(モノリシック)
├── Sources/
│ ├── Domain/ # ドメイン層
│ ├── Infrastructure/
│ ├── Orchestration/
│ └── Presentation/
└── Tests/
├── DomainTests/
├── InfrastructureTests/
└── ...

利点

  1. バランス:アーキテクチャの強制と開発の柔軟性の間の良いバランス
  2. 基盤の安定性:コア層と共通層は厳格なパッケージ境界を持つ
  3. アプリケーションの柔軟性:アプリケーション固有のレイヤーはより簡単なリファクタリングが可能
  4. 再利用性:基盤パッケージはプロジェクト間で共有できる
  5. 選択的テスト分離:基盤レイヤーは分離されたテストを持つ
  6. 複雑さの妥協:完全なパッケージアプローチよりも複雑さが少ない

欠点

  1. 部分的な強制:一部のアーキテクチャ境界のみが厳格に強制される
  2. 混合開発モデル:開発者はパッケージとモノリシックコードの両方で作業する必要がある
  3. 不整合な構造:異なるレイヤーに対して異なるアプローチを持つことが潜在的に混乱を招く
  4. まだパッケージのオーバーヘッドがある:まだいくつかのパッケージの管理が必要

使用するタイミング

  • 中規模のアプリケーション
  • パッケージベースのアプローチに移行中のチーム
  • コア層と共通層が安定しているがその他のレイヤーが頻繁に進化するプロジェクト
  • 同じ基盤を共有するが実装が異なる複数のプロジェクト
  • 完全なパッケージの複雑さなしにいくつかのアーキテクチャ強制を望むチーム

アプローチ4:ハイブリッド - 機能ベースのパッケージ

このアプローチでは、すべてのレイヤーにわたる垂直スライスを含む機能ベースのパッケージにコードを整理します。

構造

PrismProject/
├── CorePackage/ # コア層 - 共有
├── CommonPackage/ # 共通層 - 共有
├── FeatureAPackage/ # 完全な機能垂直スライス
│ ├── Sources/
│ │ ├── Domain/ # 機能Aのドメイン
│ │ ├── Infrastructure/ # 機能Aのインフラストラクチャ
│ │ ├── Orchestration/ # 機能Aのオーケストレーション
│ │ └── Presentation/ # 機能Aのプレゼンテーション
│ └── Tests/
├── FeatureBPackage/ # 別の機能垂直スライス
└── MainApp/ # アプリエントリーポイントと共有リソース

利点

  1. 機能分離:完全な機能が独自のパッケージに分離される
  2. チームアライメント:チームはレイヤーではなく機能全体を所有できる
  3. 複合アプローチ:各機能内でレイヤー構造を維持する
  4. 並行開発:機能は並行して開発できる
  5. 機能再利用性:機能はアプリケーション間で再利用できる可能性がある
  6. モジュラーテスト:機能は分離してテストできる

欠点

  1. 潜在的な重複:機能間でコードの重複につながる可能性
  2. レイヤーの一貫性:レイヤー間で一貫した実装を確保するのが難しい
  3. 機能間の依存関係:機能間の依存関係の管理が課題となる可能性
  4. 構造の複雑さ:理解と維持がより複雑な構造
  5. レイヤーの可視性:レイヤー構造が機能構造に対して二次的になる可能性

使用するタイミング

  • 明確な機能を持つ大規模なアプリケーション
  • 機能ベースのチームを持つプロジェクト
  • 機能が相互に関連するよりも独立している傾向が強いアプリケーション
  • 機能が独立して有効/無効にされる可能性があるプロジェクト
  • チーム構造がアーキテクチャレイヤーよりも機能に沿っている組織

レイヤー固有の編成

全体的なプロジェクト構造を超えて、各レイヤーは特定の関心事を反映した独自の内部編成を持つべきです。

コア層の編成

Core/
├── Entities/ # ドメインエンティティ
│ ├── BaseEntityProtocol.swift
│ ├── User/
│ │ └── User.swift
│ ├── Product/
│ │ └── Product.swift
│ └── ...
├── ValueObjects/ # 値オブジェクト
│ ├── BaseValueObjectProtocol.swift
│ ├── Money.swift
│ ├── EmailAddress.swift
│ └── ...
├── Enumerations/ # ドメイン固有の列挙型
│ ├── ProductCategory.swift
│ ├── OrderStatus.swift
│ └── ...
└── Protocols/ # コアプロトコル
├── Repositories/
│ ├── UserRepositoryProtocol.swift
│ ├── ProductRepositoryProtocol.swift
│ └── ...
└── Services/
├── AuthenticationServiceProtocol.swift
└── ...

ドメイン層の編成

Domain/
├── Services/ # ドメインサービス
│ ├── UserService.swift
│ ├── ProductService.swift
│ └── ...
├── Validation/ # バリデーションサービス
│ ├── UserValidationService.swift
│ ├── ProductValidationService.swift
│ └── ...
├── Rules/ # ビジネスルール
│ ├── PricingRules.swift
│ ├── DiscountRules.swift
│ └── ...
└── Events/ # ドメインイベント
├── UserEvents.swift
├── OrderEvents.swift
└── ...

オーケストレーション層の編成

Orchestration/
├── UseCases/ # エンティティ中心のユースケース
│ ├── User/
│ │ ├── RegisterUserUseCase.swift
│ │ ├── UpdateUserProfileUseCase.swift
│ │ └── ...
│ ├── Product/
│ │ ├── CreateProductUseCase.swift
│ │ ├── UpdateProductUseCase.swift
│ │ └── ...
│ └── ...
├── Services/ # オーケストレーションサービス
│ ├── AuthenticationOrcService.swift
│ ├── CatalogOrcService.swift
│ └── ...
├── Workflows/ # 複雑なワークフロー
│ ├── CheckoutWorkflow.swift
│ ├── OnboardingWorkflow.swift
│ └── ...
└── EventHandlers/ # ドメインイベントハンドラー
├── UserEventHandlers.swift
├── OrderEventHandlers.swift
└── ...

インフラストラクチャ層の編成

Infrastructure/
├── DataAccess/ # データアクセスコンポーネント
│ ├── Repositories/ # リポジトリ実装
│ │ ├── UserRepository.swift
│ │ ├── ProductRepository.swift
│ │ └── ...
│ ├── QueryServices/ # 特殊クエリサービス
│ │ ├── UserQueryService.swift
│ │ ├── ProductQueryService.swift
│ │ └── ...
│ ├── REST/ # REST APIコンポーネント
│ │ ├── RestClient.swift
│ │ ├── Endpoints/
│ │ └── ...
│ └── GraphQL/ # GraphQLコンポーネント
│ ├── GraphQLClient.swift
│ ├── Operations/
│ └── ...
├── SystemServices/ # システムサービス統合
│ ├── NotificationService.swift
│ ├── LocationService.swift
│ └── ...
└── ExternalServices/ # 外部サービス統合
├── PaymentGatewayService.swift
├── AnalyticsService.swift
└── ...

プレゼンテーション層の編成

Presentation/
├── Features/ # 機能固有のUI
│ ├── Auth/ # 認証機能
│ │ ├── Login/
│ │ │ ├── LoginView.swift
│ │ │ ├── LoginPresenter.swift
│ │ │ ├── LoginState.swift
│ │ │ └── LoginIntent.swift
│ │ ├── Register/
│ │ └── ...
│ ├── Catalog/ # 製品カタログ機能
│ │ ├── List/
│ │ ├── Detail/
│ │ └── ...
│ └── ...
├── Components/ # 共有UIコンポーネント
│ ├── Buttons/
│ ├── Cards/
│ ├── Lists/
│ └── ...
├── Navigation/ # ナビゲーションコンポーネント
│ ├── Router.swift
│ ├── NavigationCoordinator.swift
│ └── ...
└── Utils/ # UIユーティリティ
├── FormatterUtils.swift
├── UIExtensions.swift
└── ...

ファイル内部の編成

フォルダ構造を超えて、ファイル内の一貫した編成は保守性にとって重要です:

プロトコル優先パターン

プロトコルを実装の前に配置します:

// Core/Protocols/Repositories/ProductRepositoryProtocol.swift
public protocol ProductRepositoryProtocol {
func getProduct(id: ProductID) async throws -> Product?
func getProductList() async throws -> [Product]
func saveProduct(_ product: Product) async throws
}

// Infrastructure/DataAccess/Repositories/ProductRepository.swift
public final class ProductRepository: ProductRepositoryProtocol {
// 実装の詳細...
}

標準ファイル構造

一貫性のために、各ファイルは標準的な構造に従うべきです:

  1. インポート:ファイルの最上部
  2. 型宣言:プロトコル、クラス、構造体、列挙型
  3. プロパティ:定数と変数
  4. 初期化:イニシャライザとセットアップメソッド
  5. パブリックインターフェース:パブリックメソッドと関数
  6. プライベートヘルパー:プライベート実装の詳細
  7. 拡張:主要な型の拡張

ファイル命名規則

一貫したファイル命名は発見しやすさと明瞭さを向上させます:

  • エンティティ[EntityName].swift(例:Product.swift
  • 値オブジェクト[ValueObjectName].swift(例:Money.swift
  • プロトコル[Name]Protocol.swift(例:ProductRepositoryProtocol.swift
  • ユースケース[Operation][EntityName]UseCase.swift(例:CreateProductUseCase.swift
  • サービス[Domain][Service].swift(例:ProductPriceService.swift
  • オーケストレーションサービス[Feature]OrcService.swift(例:CatalogOrcService.swift
  • リポジトリ[EntityName]Repository.swift(例:ProductRepository.swift
  • プレゼンター[Feature]Presenter.swift(例:ProductListPresenter.swift
  • ビュー[Feature]View.swift(例:ProductListView.swift
  • ステート[Feature]State.swift(例:ProductListState.swift
  • インテント[Feature]Intent.swift(例:ProductListIntent.swift

テスト編成

テスト編成は、テスト固有の懸念事項を追加しながら本番コード構造を反映する必要があります:

レイヤーごとのパッケージテスト構造

CorePackage/
├── Sources/
│ └── ...
└── Tests/
├── Entities/
│ ├── ProductTests.swift
│ └── ...
├── ValueObjects/
│ ├── MoneyTests.swift
│ └── ...
└── Mocks/
├── MockProductRepository.swift
└── ...

モノリシックテスト構造

PrismProject/
├── Sources/
│ └── ...
└── Tests/
├── CoreTests/
│ ├── Entities/
│ ├── ValueObjects/
│ └── Mocks/
├── DomainTests/
│ ├── Services/
│ ├── Validation/
│ └── Mocks/
└── ...

決定要因

Prismアーキテクチャのプロジェクト構造を選択する際に、これらの要因を考慮してください:

チームの規模と組織

  • 小規模チーム(1-3人):モノリシックまたはハイブリッドアプローチを検討
  • 中規模チーム(4-8人):ハイブリッドアプローチを検討
  • 大規模チーム(9人以上):レイヤーごとのパッケージまたは機能ベースのパッケージを検討

プロジェクトの規模と複雑さ

  • 小規模プロジェクト:モノリシック構造で通常は十分
  • 中規模プロジェクト:ハイブリッドアプローチが良いバランスを提供
  • 大規模プロジェクト:完全なパッケージ分離が必要な境界を提供

アーキテクチャ強制の優先度

  • 高優先度:レイヤーごとのパッケージアプローチ
  • 中優先度:基盤パッケージを持つハイブリッド
  • 低優先度:良い規約を持つモノリシック構造

開発速度の要件

  • 速度重視:モノリシックまたは最小限のハイブリッド
  • バランス:ハイブリッドアプローチ
  • 構造重視:完全なパッケージ分離

再利用性のニーズ

  • 高い再利用性:レイヤーごとのパッケージまたは機能ベースのパッケージ
  • 選択的再利用性:基盤パッケージを持つハイブリッド
  • アプリケーション固有:モノリシック構造

実装ガイドライン

選択したプロジェクト構造に関わらず、成功した実装のためにこれらのガイドラインに従いましょう:

レイヤー境界の強制

  1. 明確なインポート文:インポートを通じて依存関係を明示的にする
  2. アクセス制御:適切な場合はアクセス修飾子を使用して可視性を制限する
  3. 依存性注入:グローバルを使用するのではなく、依存関係を明示的に渡す
  4. レイヤー固有のテスト:適切なモックを使用して各レイヤーを分離してテストする

ファイルと型の編成

  1. 論理的なグループ化:関連するファイルを目的を反映したフォルダにグループ化する
  2. 一貫した命名:コードベース全体で命名規則に一貫して従う
  3. 型の配置:密接に結合している場合を除き、各型は独自のファイルに置く
  4. ドキュメント:各ファイルの目的を説明するヘッダードキュメントを含める

プロジェクト進化戦略

  1. シンプルに始める:よりシンプルな構造から始め、必要に応じて進化させる
  2. 段階的なモジュール化:パッケージをコア層と共通層から始めて一つずつ抽出する
  3. リファクタリングの期間:構造的リファクタリングのための専用の時間を設ける
  4. ドキュメント:プロジェクト構造のドキュメントを最新の状態に保つ

避けるべき一般的なアンチパターン

構造的アンチパターン

  1. レイヤー違反:利便性のためにアーキテクチャ境界をバイパスする
  2. 誤ったレイヤー配置:ロジックを誤ったアーキテクチャレイヤーに配置する
  3. 循環依存関係:パッケージ間の相互依存関係を作成する
  4. 構造の不整合:プロジェクト全体で異なる編成パターンを使用する
  5. モノリシックパッケージ:大きすぎて無関係な機能を含むパッケージ

組織的アンチパターン

  1. 機能/レイヤーの不一致:チーム組織と一致しないプロジェクト構造
  2. 過剰な設計:シンプルなアプリケーションに対する複雑な構造
  3. 早すぎるモジュール化:境界が明確になる前にパッケージに分割する
  4. 一貫性のない規約:命名と編成パターンの混在

結論

Prismアーキテクチャプロジェクトの物理的な構造は、長期的な保守性と開発効率において重要な役割を果たします。最適な構造は、チームの規模、プロジェクトの複雑さ、および組織のニーズを含む特定のコンテキストによって異なります。

ほとんどのプロジェクトでは、ハイブリッドアプローチが良いバランスを提供します—安定した基盤レイヤー(コアと共通)にはパッケージを使用し、アプリケーション固有のレイヤーはより柔軟なモノリシック構造に保ちます。プロジェクトの規模と複雑さが増すにつれて、よりパッケージベースのアプローチに移行することがしばしば理にかなっています。

最も重要な要素は一貫性です—選択されたアプローチは一貫して適用され、チームに明確に文書化されるべきです。プロジェクト構造はアーキテクチャの目標をサポートし、開発を妨げたり不必要な複雑さを生み出したりすべきではないことを忘れないでください。