🧅
概念 #クリーンアーキテクチャ #進化的アーキテクチャ #アーキテクチャ #設計原則 #依存関係 📚 進化的アーキテクチャ

クリーンアーキテクチャが進化耐性をもたらす理由

依存方向の原則がドメインを安定させ、DB・フレームワーク変更を局所化する。Expand-Contract・Dual Write との接続と、ユースケーステストが Fitness Function になる仕組みを整理する

依存方向の原則

クリーンアーキテクチャの本質は一つの規則だ。

「依存関係は常に内側(ドメイン)に向かう。外側(インフラ)が内側を知る。内側は外側を知らない」

  外側(インフラ / フレームワーク)
  ┌─────────────────────────────────────┐
  │  Controller / Gateway / Repository  │
  │           ↓(依存する方向)          │
  │     ┌─────────────────────┐          │
  │     │  Use Cases          │          │
  │     │       ↓             │          │
  │     │  ┌─────────────┐    │          │
  │     │  │  Domain     │    │          │
  │     │  │  (Entity)  │    │          │
  │     │  └─────────────┘    │          │
  │     └─────────────────────┘          │
  │  内側(ドメイン)が変化の中心        │
  └─────────────────────────────────────┘
         ← 変化の影響は外側に留まる

インフラの詳細(どの DB を使うか、どのフレームワークか)は外側に閉じる。ドメインはそれらを知らない。

なぜドメインが安定するとアーキテクチャが進化できるか

DB を変更しても Domain は無傷

// Domain 層: DB を知らない(ドメインの言語で定義)
interface UserRepository {
  findById(id: UserId): Promise<User | null>;
  save(user: User): Promise<void>;
}

// Infrastructure 層: ドメインを知り、実装を提供する
class PostgresUserRepository implements UserRepository {
  async findById(id: UserId) { /* Postgres の詳細 */ }
  async save(user: User) { /* Postgres の詳細 */ }
}

// PostgreSQL → DynamoDB に変える場合:
class DynamoUserRepository implements UserRepository {
  async findById(id: UserId) { /* DynamoDB の詳細 */ }
  async save(user: User) { /* DynamoDB の詳細 */ }
}

// DI の設定(外側)を変えるだけ。ドメインは一行も変わらない。

フレームワーク変更が局所化される

Express → Hono → Elysia へ変えても、Controller 層だけを書き換えればよい。Use Case / Domain は変わらない。

// Use Case(フレームワークを知らない)
class CreateOrderUseCase {
  async execute(command: CreateOrderCommand): Promise<Order> {
    const order = Order.create(command);
    await this.orderRepo.save(order);
    return order;
  }
}

// Express の Controller(外側)
app.post('/orders', async (req, res) => {
  const order = await createOrderUseCase.execute(req.body);
  res.status(201).json(order);
});

// Hono の Controller(外側)← ここだけ変わる
app.post('/orders', async (c) => {
  const order = await createOrderUseCase.execute(await c.req.json());
  return c.json(order, 201);
});

Expand-Contract・Dual Write との接続

Expand-Contract パターンと Dual Write はインフラ層(外側)で完結する。ドメインを汚さない。

Domain 層(変わらない)
  ├─ User エンティティ
  └─ UserRepository インタフェース(findById / save のみ)

Infrastructure 層(ここで Expand-Contract を実施)
  └─ PostgresUserRepository
       ├─ Phase 1 (Expand): email と email_new の両方に書く
       ├─ Phase 2 (Migrate): バックフィル
       └─ Phase 3 (Contract): email_new だけ読み書き、email カラム削除

ドメイン層の UserRepository インタフェースは save(user: User) のままで変わらない。DB 移行の詳細は Repository 実装の中だけで変わる。

Fitness Function = ユースケーステストがドメインを守る

クリーンアーキテクチャではユースケーステストが書きやすい。インフラなし(In-Memory Repository)でドメインの振る舞いを高速に検証できる。

これは進化的アーキテクチャの Fitness Function として機能する。

// ユースケーステスト = ドメインを守る Fitness Function
describe('CreateOrderUseCase', () => {
  it('在庫がない場合は注文できない', async () => {
    const inventory = new InMemoryInventory({ itemId: 'A', stock: 0 });
    const useCase = new CreateOrderUseCase(
      new InMemoryOrderRepository(),
      inventory,
    );

    await expect(
      useCase.execute({ items: [{ itemId: 'A', qty: 1 }] })
    ).rejects.toThrow(OutOfStockError);
    // DB なし・ネットワークなしで高速に実行できる
    // 変更のたびに CI でこのテストが走り、ドメインルールを守る
  });
});

レイヤードアーキテクチャとの進化耐性の比較

観点レイヤードアーキテクチャクリーンアーキテクチャ
依存の方向上から下(Domain が Infra を知ることがある)常に内側(Domain は Infra を知らない)
DB 変更の影響範囲Domain まで波及しやすいRepository 実装だけで完結
フレームワーク変更の影響Service 層まで影響することがあるController 層だけ
テストの書きやすさDB モックが必要になりやすいIn-Memory で高速テスト可
進化的アーキテクチャとの親和性高い(外側が変わってもドメインは不変)

「Clean Architecture だから変化に強い」ではない

注意点がある。クリーンアーキテクチャはあくまで依存方向の原則だ。ドメインロジックが肥大化したり、インタフェースが多すぎて変更しにくくなったりすると逆効果になる。

進化耐性を高めるのは「依存方向の原則」+「Fitness Function(ユースケーステスト)」+「段階的変更」の組み合わせだ。

関連概念

出典・参考文献

  • Robert C. Martin, Clean Architecture: A Craftsman’s Guide to Software Structure and Design (2017)
  • Neal Ford et al., Building Evolutionary Architectures (2022) Chapter 4

出典: Clean Architecture(Robert C. Martin)/ 進化的アーキテクチャ(O'Reilly)