コンテンツにスキップ

クリーンアーキテクチャ

はじめに

本サイトにつきまして、【IT技術の知見】はじめに - 俺の技術ノート をご認識のほど宜しくお願いいたします。


01. アーキテクチャ概要

思想

ドメイン駆動設計が適する機能的アプリケーションのみでなく、あらゆる種類 (例:非機能的アプリケーションなど) のソフトウェアに適用できる。

クリーンアーキテクチャ原著の序文にて、著者は「私は今までいろいろな種類のシステムを作ってきたが、どのシステムでもアーキテクチャのルールは同じだった」と述べている。

異なるシステムでも同じルールを共有する必要がある』というようなことを述べている。


構成

クリーンアーキテクチャは同心円状の層で構成され、外側の層から内側の層に向かってのみ依存する。

clean-architecture

各パターンの分類

オブジェクト指向型と手続き型のマルチパラダイム言語の場合、アプリケーションをクラスまたは関数で実装できる。

クリーンアーキテクチャはオブジェクト指向設計が前提にあるため、各レイヤーのパターンをクラスとして実装することが多い。

ただし、クラスとして実装するとインスタンス化の手間がある。関数として実装しても、アーキテクチャの思想に反しない場合がある (特に外側のレイヤー) 。

アーキテクチャのレイヤー別にこれを整理した。

バックエンドレイヤー パターン 責務 インターフェース/実装
インフラストラクチャ層 DB データベースとの接続、コネクションの作成 実装
EntityMappers (DTO に相当) DB カラム型からアプリデータ型への型変換(例:BIGINT や DECIMAL を情報量が減らないデータ型に変換)、ドメインとインフラ間のオブジェクトの詰め替え 実装
Logger ロギング 実装
Middleware ミドルウェアパターン 実装
Repositories 実装リポジトリ 実装
Listeners リスナー 関数
Routers ルーティング 関数
Seeder 開発環境、CI 環境のユニットテストや E2E テスト、ステージング環境の動作確認に使用する初期データ挿入 実装
Drivers SDK、ORM、CLI など 実装
プレゼンテーション層 Controller データの受信と返信 実装
RequestValidators 受信データのバリデーション 実装
ResponseValidators 返信データのバリデーション (受信データと比べてデータ構造は保証されているため、なくてもいい) 実装
Authenticators 認証 実装
Input (Command とも呼ぶ) 型変換、入力データ翻訳、フロントエンドインターフェース (あるいは受信 JSON データ) からバックエンドインターフェースへのオブジェクトへの詰め替え 実装
Output (DTO とも呼ぶ) 型変換、出力データ翻訳、バックエンドインターフェースからフロントエンドインターフェースへのオブジェクトへの詰め替え 実装
ユースケース層 Interactor ドメイン層で定義されたドメインルールを組み合わせ、ユースケースを具現化する。 実装
InputBoundaries Interactor のインターフェース インターフェース
Input (Command とも呼ぶ) 型変換、バックエンドインターフェースからユースケースのオブジェクトへの詰め替え 実装
OutputBoundaries Presenter のインターフェース インターフェース
Output (DTO とも呼ぶ) 型変換、ユースケースからフロントエンドインターフェースへのオブジェクトへの詰め替え 実装
Repositories(Gateways とも呼ぶ) Repositories のインターフェース インターフェース
ドメイン層 Entity ドメインモデル、ドメインルールの定義 実装
Id ドメインモデルの識別子 実装
ValueObject 値オブジェクト 実装
Specification ビジネスロジックのバリデーション 実装
Criterion 検索条件オブジェクト 実装
Events ドメインイベント 実装
Service ドメインサービス 実装
フロントエンドレイヤー パターン 責務 インターフェース/実装
プレゼンテーション層 Presenter ドメインオブジェクトから JSON データへの詰め替え 実装
Validators フロントエンドのバリデーション 実装
ViewModel 状態管理、JSON データをフロントエンドインターフェースのオブジェクトへの詰め替え 実装
UI 層 View UI レンダリング、CSS スタイリング 実装


実装例たち

言語 あれば書籍名 URL
TypeScript つくりながら学ぶ! ドメイン駆動設計 実践入門 https://github.com/yamachan0625/hands-on-ddd-introduction/tree/main/chapter-21
Java なし https://github.com/VaughnVernon/IDDD_Samples
Go なし https://github.com/victorsteven/food-app-server
C# ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 https://github.com/nrslib/itddd
そのほか なし https://github.com/stars/hiroki-it/lists/ddd-architecture


02. 依存方向の制御

ドキュメント

レイヤー間の依存関係の方向をドキュメントで明文化する。

graph LR

  %% インターフェース層
  %% リクエストの受信やレスポンスの返信に関するロジックを置く
  subgraph controllers ["【インターフェース層】./controllers"]
    controller
  end

  %% ユースケース層
  %% ドメイン層のロジックを組み合わせ、ユースケースを実現するロジックを置く
  subgraph usecases ["【ユースケース層】./usecases"]
    usecase
  end

  %% ドメイン層
  %% ドメインルールに関するロジックと、リポジトリのインターフェースを置く
  subgraph domain ["【ドメイン層】./domain"]
    domainObject
    repositoryInterface
  end

  %% インフラストラクチャ層
  %% データベースへの接続や、リポジトリの実装に関するロジックを置く
  subgraph infrastructure ["【インフラストラクチャ層】./infrastructure"]
    repository
    database
  end

  controllers --> usecases
  usecases --> domain
  infrastructure -->|インターフェースを実装| domain
  repository -->|CRUD| database


静的解析

レイヤー間の依存関係の方向を静的解析で制御する。

例えば、Go では golangci-lint の depguard、TypeScript では eslint-plugin-import プラグインの import/no-restricted-paths ルール、PHP では Deptrac が役立つ。

.yml
version: "2"

linters:
  enable:
    - depguard
  settings:
    depguard:
      rules:
        domain:
          # 対象のファイル
          files:
            - "**/internal/domain/**"
            - "!$test"
          # 依存禁止のパッケージ
          deny:
            # アプリケーション層に依存してはいけない
            - pkg: "example.com/foo/internal/application"
              desc: "Domain層でApplication層をimportしてはいけません。"
            # プレゼンテーション層に依存してはいけない
            - pkg: "example.com/foo/internal/presentation"
              desc: "Domain層でPresentation層をimportしてはいけません。"
            # インフラストラクチャ層に依存してはいけない
            - pkg: "example.com/foo/internal/infrastructure"
              desc: "Domain層でInfrastructure層をimportしてはいけません。"
        application:
          # 対象のファイル
          files:
            - "**/internal/application/**"
            - "!$test"
          # 依存禁止のパッケージ
          deny:
            # プレゼンテーション層に依存してはいけない
            - pkg: "example.com/foo/internal/presentation"
              desc: "Application層でPresentation層をimportしてはいけません。"
            # インフラストラクチャ層に依存してはいけない
            - pkg: "example.com/foo/internal/infrastructure"
              desc: "Application層でInfrastructure層をimportしてはいけません。"
TypeScript の場合の実装例
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import * as importPlugin from "eslint-plugin-import";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,

  {
    files: ["**/*.ts"],
    plugins: {
      import: importPlugin,
    },
    settings: {
      "import/resolver": {
        typescript: {},
      },
    },
    rules: {
// (略)
      "import/no-restricted-paths": [
        "error",
        {
          zones: [
            {
              // 依存禁止のパス
              // アプリケーション層に依存してはいけない
              from: "./src/Application/**/*",
              // 対象のファイル
              target: "./src/Domain/**/!(*.spec.ts|*.test.ts)",
              message:
                "Domain層でApplication層をimportしてはいけません。",
            },
            {
              // 依存禁止のパス
              // プレゼンテーション層に依存してはいけない
              from: "./src/Presentation/**/*",
              // 対象のファイル
              target: "./src/Domain/**/!(*.spec.ts|*.test.ts)",
              message:
                "Domain層でPresentation層をimportしてはいけません。",
            },
            {
              // 依存禁止のパス
              // インフラストラクチャ層に依存してはいけない
              from: "./src/Infrastructure/**/*!(test).ts",
              // 対象のファイル
              target: "./src/Domain/**/!(*.spec.ts|*.test.ts)",
              message:
                "Domain層でInfrastructure層をimportしてはいけません。",
            },

            {
              // 依存禁止のパス
              // プレゼンテーション層に依存してはいけない
              from: "./src/Presentation/**/*",
              // 対象のファイル
              target: "./src/Application/**/!(*.spec.ts|*.test.ts)",
              message:
                "Application層でPresentation層をimportしてはいけません。",
            },
            {
              // 依存禁止のパス
              // インフラストラクチャ層に依存してはいけない
              from: "./src/Infrastructure/**/*",
              // 対象のファイル
              target: "./src/Application/**/!(*.spec.ts|*.test.ts)",
              message:
                "Application層でInfrastructure層をimportしてはいけません。",
            },
          ],
        },
      ],
    },
  }
);
.yaml
deptrac:
  paths:
    - ./src
  # 層とそのディレクトリ
  layers:
    - name: Domain
      collectors:
        - type: directory
          value: src/Domain/.*
    - name: Application
      collectors:
        - type: directory
          value: src/Application/.*
    - name: Presentation
      collectors:
        - type: directory
          value: src/Presentation/.*
    - name: Infrastructure
      collectors:
        - type: directory
          value: src/Infrastructure/.*
  # 依存してよい層
  ruleset:
    # ドメイン層は、ほかの層に依存してはいけない
    Domain: ~
    # アプリケーション層は、ドメイン層にのみ依存してよい
    Application:
      - Domain
    Presentation:
      - Application
      - Domain
    Infrastructure:
      - Application
      - Domain