📖

クリーンアーキテクチャ入門 Part 1: 基礎概念と4つの層の全体像

に公開

クリーンアーキテクチャ入門 Part 1: 基礎概念と4つの層の全体像

はじめに

クリーンアーキテクチャは、ソフトウェアの保守性と拡張性を向上させるための設計パターンです。このシリーズでは、実際のRustプロジェクトのコードを例に、段階的にクリーンアーキテクチャを学んでいきます。

この記事で学べること:

  • クリーンアーキテクチャの基本概念
  • 4つの層の役割と関係性
  • 依存関係の方向性
  • 実際のプロジェクト構造

クリーンアーキテクチャとは

クリーンアーキテクチャは、内側の層が外側の層に依存しない設計原則に基づいています。これにより、ビジネスロジックが技術的な詳細から独立し、テストしやすく、変更しやすいソフトウェアを構築できます。

なぜクリーンアーキテクチャが必要なのか?

従来の設計の問題点:

  • ビジネスロジックとデータベース処理が混在
  • テストが困難(実際のデータベースが必要)
  • 技術スタックの変更が困難
  • 機能追加時に既存コードへの影響が大きい

クリーンアーキテクチャの解決策:

  • ビジネスロジックの独立
  • テストの容易さ
  • 技術スタックの変更が容易
  • 機能追加の影響範囲を限定

4つの層の全体像

クリーンアーキテクチャは、内側から外側に向かって依存関係が流れる4つの層で構成されています:

┌─────────────────────────────────────┐
│           Presentation Layer        │ ← ユーザーインターフェース
├─────────────────────────────────────┤
│         Application Layer           │ ← ビジネスロジックの調整
├─────────────────────────────────────┤
│           Domain Layer              │ ← ビジネスルール(中心)
├─────────────────────────────────────┤
│        Infrastructure Layer         │ ← 外部システムとの連携
└─────────────────────────────────────┘

各層の役割

1. Domain Layer(ドメイン層)- 最も内側の層

  • ビジネスルールの定義:アプリケーションの核心となるビジネスロジック
  • エンティティの定義:ビジネスオブジェクトの構造と振る舞い
  • インターフェースの定義:他の層との契約

2. Application Layer(アプリケーション層)

  • ユースケースの実装:特定のビジネスシナリオの実行
  • ドメイン層の調整:複数のドメインオブジェクトを組み合わせ
  • データの加工・変換:ビジネス要件に合わせたデータ処理

3. Infrastructure Layer(インフラストラクチャ層)

  • 外部システムとの連携:データベース、外部API、ファイルシステム
  • ドメイン層のインターフェース実装:リポジトリパターンの実装
  • 技術的な詳細の隠蔽:SQL、HTTP通信などの詳細を隠す

4. Presentation Layer(プレゼンテーション層)

  • ユーザーインターフェース:HTTP API、Web UI、CLI
  • リクエスト・レスポンスの処理:データの変換と検証
  • アプリケーション層の呼び出し:ユースケースの実行

依存関係の方向性

クリーンアーキテクチャの核心は、依存関係の方向性にあります:

Presentation Layer → Application Layer → Domain Layer ← Infrastructure Layer

重要な原則

  1. 内側の層は外側の層を知らない

    • Domain層はInfrastructure層の存在を知らない
    • Application層はPresentation層の詳細を知らない
  2. 依存関係は内側に向かって流れる

    • 外側の層は内側の層に依存できる
    • 内側の層は外側の層に依存してはいけない
  3. インターフェースによる契約

    • 具体的な実装ではなく、抽象(インターフェース)に依存
    • 各層は明確な契約(インターフェース)を通じて通信

実際のプロジェクト構造

実際のRustプロジェクトでのディレクトリ構造を見てみましょう:

src/
├── domain/                    # ドメイン層
│   ├── entities/             # ビジネスオブジェクト
│   │   ├── task.rs
│   │   └── user.rs
│   ├── repositories/         # リポジトリインターフェース
│   │   ├── task_repository.rs
│   │   └── user_repository.rs
│   └── services/             # ドメインサービス
│       └── task_domain_service.rs
├── application/              # アプリケーション層
│   ├── use_cases/           # ユースケース
│   │   ├── task/
│   │   │   ├── add_task/
│   │   │   └── list_tasks/
│   │   └── user/
│   │       └── register_user/
│   └── adapters/            # アダプター
│       ├── task_adapter.rs
│       └── user_adapter.rs
├── infrastructure/           # インフラストラクチャ層
│   ├── repositories/        # リポジトリ実装
│   │   ├── task_repository_impl.rs
│   │   └── user_repository_impl.rs
│   ├── external_apis/       # 外部API連携
│   │   └── notification_service.rs
│   └── database/            # データベース関連
│       ├── models.rs
│       └── migrations/
└── presentation/            # プレゼンテーション層
    ├── controllers/         # コントローラー
    │   ├── task_controller.rs
    │   └── user_controller.rs
    ├── handlers/            # HTTPハンドラー
    │   ├── task_handlers.rs
    │   └── user_handlers.rs
    └── presenters/          # プレゼンター
        ├── task_presenter.rs
        └── user_presenter.rs

基本的な実装例

Domain層の例

// src/domain/entities/task.rs
#[derive(Debug, Clone)]
pub struct Task {
    pub id: Option<i32>,
    pub title: String,
    pub description: String,
    pub status: TaskStatus,
}

#[derive(Debug, Clone, PartialEq)]
pub enum TaskStatus {
    Pending,
    InProgress,
    Completed,
    Cancelled,
}

impl Task {
    pub fn new(title: String, description: String) -> Result<Self, Box<dyn Error + Send + Sync>> {
        if title.trim().is_empty() {
            return Err("Task title cannot be empty".into());
        }

        Ok(Task {
            id: None,
            title: title.trim().to_string(),
            description: description.trim().to_string(),
            status: TaskStatus::Pending,
        })
    }
}

// src/domain/repositories/task_repository.rs
#[async_trait]
pub trait TaskRepository {
    async fn save(&self, task: &Task) -> Result<Task, Box<dyn Error + Send + Sync>>;
    async fn find_by_id(&self, id: i32) -> Result<Option<Task>, Box<dyn Error + Send + Sync>>;
}

Application層の例

// src/application/use_cases/task/add_task/add_task_use_case.rs
pub struct AddTaskUseCase {
    task_repository: Arc<dyn TaskRepository + Send + Sync>,
}

impl AddTaskUseCase {
    pub fn new(task_repository: Arc<dyn TaskRepository + Send + Sync>) -> Self {
        Self { task_repository }
    }

    pub async fn execute(&self, input: AddTaskInput) -> Result<AddTaskOutput, Box<dyn Error + Send + Sync>> {
        let task = Task::new(input.title, input.description)?;
        let saved_task = self.task_repository.save(&task).await?;

        Ok(AddTaskOutput {
            id: saved_task.id.unwrap(),
            title: saved_task.title,
            description: saved_task.description,
        })
    }
}

初心者向けの学習アプローチ

Step 1: 基本的な層分離を理解する

まずは4つの層の役割を理解し、ディレクトリ構造を作成することから始めましょう。

Step 2: 依存関係の方向性を意識する

内側の層が外側の層に依存しないことを常に意識してください。

Step 3: インターフェースを活用する

具体的な実装ではなく、インターフェースに依存する設計を心がけてください。

Step 4: 段階的に実装する

完璧を求めすぎず、小さな機能から始めて段階的に拡張していきましょう。

次のステップ

この記事でクリーンアーキテクチャの基本概念を理解できたら、次は以下の記事に進んでください:

  • Part 2: ビジネスロジックの設計(Domain層・Application層)
  • Part 3: 外部システムとの連携(Infrastructure層・Presentation層)
  • Part 4: 実践的な開発フローとAI活用

まとめ

クリーンアーキテクチャの基本は以下の3つの原則です:

  1. 層の分離:各層が明確な責任を持つ
  2. 依存関係の方向性:内側に向かって依存する
  3. インターフェースによる契約:具体的な実装ではなく抽象に依存

これらの原則を理解することで、保守性が高く、拡張しやすいソフトウェアを構築できるようになります。

次回のPart 2では、Domain層とApplication層の詳細な実装について学んでいきます。

コラボスタイル Developers

Discussion