lvncerpedia

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

Robert C. Martin(通称Uncle Bob)が提唱した、ソフトウェアの関心事を層に分離する設計思想。

核心的な目的はこれ:

  • ビジネスロジックをフレームワーク・DB・UIから独立させる
  • テストしやすくする
  • 変更に強くする

有名な「同心円図」

          ┌─────────────────────────────────┐
          │         Frameworks & Drivers     │  ← Web, DB, UI, External
          │   ┌─────────────────────────┐   │
          │   │      Interface Adapters  │   │  ← Controllers, Presenters, Gateways
          │   │   ┌─────────────────┐   │   │
          │   │   │  Application    │   │   │  ← Use Cases
          │   │   │  Business Rules │   │   │
          │   │   │  ┌───────────┐  │   │   │
          │   │   │  │  Domain   │  │   │   │  ← Entities
          │   │   │  │  Entities │  │   │   │
          │   │   │  └───────────┘  │   │   │
          │   │   └─────────────────┘   │   │
          │   └─────────────────────────┘   │
          └─────────────────────────────────┘

          依存の方向:外側 → 内側(内側は外側を知らない)

各層の責務

層別名役割具体例
EntitiesDomain層ビジネスの核心ルールUser, Order, Moneyオブジェクト
Use CasesApplication層アプリ固有のビジネスロジックCreateUserUseCase, PlaceOrderUseCase
Interface AdaptersAdapter層内外のデータ変換Controller, Repository実装, Presenter
Frameworks & DriversInfrastructure層技術的な詳細Express, MySQL, React, S3

超重要:依存性逆転の原則(DIP)

これがクリーンアーキテクチャの一番の肝。

普通に書くと Use Case が DB に依存してしまう:

❌ 普通の依存関係

UseCase ──depends on──▶ MySQLRepository(具体クラス)

クリーンアーキテクチャではインターフェースを挟む:

✅ 依存を逆転させる

UseCase ──depends on──▶ IUserRepository(interface)
                               ▲
                               │ implements
                        MySQLRepository(具体クラス)

こうすると UseCase は MySQL の存在を知らなくて済む。 PostgreSQL に変えても、Use Case は一切触らなくていい!


データフローの全体像

HTTP Request
    │
    ▼
[Controller]          ← Input変換
    │ InputDTO
    ▼
[UseCase]             ← ビジネスロジック実行
    │  │
    │  └──▶ [IRepository] ──▶ [Repository実装] ──▶ DB
    │            (interface)      (Adapter層)
    │ OutputDTO
    ▼
[Presenter]           ← Output変換
    │
    ▼
HTTP Response

DTO(データ転送オブジェクト)の役割

層をまたぐときは生のEntityをそのまま渡さない。DTOに変換する。

外側の層のデータ形式
    └▶ InputDTO ──▶ UseCase ──▶ OutputDTO ──▶ 外側の層のデータ形式
                       │
                   Entityに変換して処理

なぜ?→ Entity の変更が外側に波及しないようにするため


コード例(TypeScript風)

// ① Entity(最内層 - 純粋なビジネスルール)
class User {
  constructor(
    public readonly id: string,
    public readonly email: string,
    public readonly name: string,
  ) {}

  isValidEmail(): boolean {
    return this.email.includes('@');
  }
}

// ② Repository Interface(Use Case層が定義)
interface IUserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

// ③ Use Case
class GetUserUseCase {
  constructor(private userRepo: IUserRepository) {} // ← interfaceに依存

  async execute(id: string): Promise<UserOutputDTO> {
    const user = await this.userRepo.findById(id);
    if (!user) throw new Error('User not found');
    return { id: user.id, name: user.name }; // OutputDTOに変換
  }
}

// ④ Repository実装(Adapter層)
class MySQLUserRepository implements IUserRepository {
  async findById(id: string): Promise<User | null> {
    // MySQLの詳細はここだけ
    const row = await db.query('SELECT * FROM users WHERE id = ?', [id]);
    return row ? new User(row.id, row.email, row.name) : null;
  }
  async save(user: User): Promise<void> { /* ... */ }
}

// ⑤ Controller(Adapter層)
class UserController {
  constructor(private useCase: GetUserUseCase) {}

  async getUser(req: Request, res: Response) {
    const result = await this.useCase.execute(req.params.id);
    res.json(result);
  }
}

よく混同される他のアーキテクチャとの比較

レイヤードヘキサゴナルクリーン
依存方向上から下外から内外から内
DIP活用弱い強い強い
UseCase明示なしなしあり
テスト容易性低〜中高高

ヘキサゴナルアーキテクチャ(ポート&アダプター)とはかなり近い思想。クリーンはUseCaseをより明示的に分離してる点が特徴。


クリーンアーキテクチャのメリット・デメリット

✅ メリット

  • DBやフレームワークを後から差し替えられる
  • UseCase単体でユニットテストが書きやすい
  • ビジネスロジックが明確に分離されてる

❌ デメリット・注意点

  • ファイル数・クラス数が増える(over-engineeringになりがち)
  • 小規模プロジェクトにはオーバースペック
  • DTOの変換コードが冗長に見える

実際のディレクトリ構成例

src/
├── domain/              ← Entities
│   ├── user.ts
│   └── order.ts
├── application/         ← Use Cases + Repository interfaces
│   ├── usecases/
│   │   └── get-user.usecase.ts
│   └── repositories/
│       └── user.repository.interface.ts
├── infrastructure/      ← 実装(DB, 外部API)
│   └── repositories/
│       └── mysql-user.repository.ts
└── presentation/        ← Controllers, Presenters
    └── controllers/
        └── user.controller.ts

何か特定の部分(テスト戦略とか、Next.jsでの適用とか)もっと深掘りしたいとこある?

いい質問!「概念は分かったけど実感わかない」ってなりがちだよね。一番体感しやすいのは**「テストを書くとき」と「DBを差し替えるとき」**かな。具体的に見てこう!


一番リアルに感じる瞬間TOP3

🥇 1位:テストでDBが要らない

クリーンアーキテクチャなしの場合:

GetUserUseCase のテストしたい
    │
    └▶ MySQLに直接依存してる
            │
            └▶ テスト実行するのにMySQLが必要!!
                    → CI環境でコケる
                    → テスト遅い
                    → データ汚染が怖い  😇

クリーンアーキテクチャありの場合:

// IUserRepository を満たすだけのモックを作る
class InMemoryUserRepository implements IUserRepository {
  private store: User[] = [];
  async findById(id: string) {
    return this.store.find(u => u.id === id) ?? null;
  }
  async save(user: User) {
    this.store.push(user);
  }
}

// テスト本体(MySQLゼロ!)
const repo = new InMemoryUserRepository();
const useCase = new GetUserUseCase(repo);
const result = await useCase.execute('user-1');

DBなしで純粋にビジネスロジックだけテストできる。これが一番体感でかい。


🥈 2位:フレームワーク移行が怖くない

例えば Express → Hono に乗り換えるとき:

変更が必要な層

┌─────────────────────────────────┐
│  Frameworks & Drivers           │  ← ✅ ここだけ変える
│   ┌─────────────────────────┐   │
│   │  Interface Adapters      │   │  ← Controller少し変える
│   │   ┌─────────────────┐   │   │
│   │   │  Use Cases       │   │   │  ← ✋ 触らない
│   │   │  ┌───────────┐  │   │   │
│   │   │  │  Entities  │  │   │   │  ← ✋ 触らない
│   │   │  └───────────┘  │   │   │
│   │   └─────────────────┘   │   │
│   └─────────────────────────┘   │
└─────────────────────────────────┘

ビジネスロジックは一行も変えなくていい。


🥉 3位:MySQL → PostgreSQL 乗り換えが1ファイルで済む

変更前                          変更後

IUserRepository (interface)     IUserRepository (interface)
      ▲                               ▲
      │ implements                    │ implements
MySQLUserRepository         PostgreSQLUserRepository  ← これだけ新しく作る

UseCase もテストも Entity もノータッチ。


実際のプロジェクトでどう使われてる?

プロジェクト規模どう使われてるか
大規模サービス(Netflix, Uber級)フルに全層適用。マイクロサービス間でも同思想
中規模SaaSUse Case + Repository interface だけ採用(部分適用)が多い
小規模正直オーバーキルなので薄いMVCで十分なことも

「部分適用」が現実では多い

厳密にやると複雑すぎるので、よく見る現実的な落とし所はこれ:

よく採用される最小セット

Controller
    │
    ▼
UseCase ◀── IRepository (interface)
                  ▲
                  │
           Repository実装(DB詳細)

Entity(Domainロジックはここに集約)

DTOを省略したり、Presenterを省いたりするのは全然アリ。


Next.jsで使うとしたら?

src/
├── domain/
│   └── user.ts                   ← Entity
├── application/
│   ├── usecases/create-user.ts   ← UseCase
│   └── repositories/i-user.ts   ← Interface
├── infrastructure/
│   └── prisma-user.repository.ts ← Prisma実装
└── app/
    └── api/users/route.ts        ← ここはAdapter層扱い

route.ts は Controller 相当で、UseCase を呼ぶだけにする。 Prisma の詳細は infrastructure/ に閉じ込める感じ。


一言でまとめると、「変更やテストのコストが下がる」という恩恵を、プロジェクトが大きくなるほど実感するアーキテクチャだよ。小さいうちは「なんでこんな複雑に…」ってなるやつ笑

もっと具体的に「このケースだったらどうする?」みたいな相談でも答えられるよ!