LESSON 30分

ストーリー

高橋アーキテクト
品質特性は定義しただけでは意味がない。継続的に監視しなければ、時間とともに劣化する
高橋アーキテクト
アーキテクチャの劣化を自動的に検出する仕組みが”フィットネス関数”だ。テストと同じように、CIで自動実行する

フィットネス関数とは

アーキテクチャフィットネス関数は、アーキテクチャの品質特性が維持されているかを自動検証する仕組みです。Neal Ford、Rebecca Parsons、Patrick Kuaらが提唱しました。

通常のテスト:
  「この機能は正しく動作するか?」 → ユニットテスト

フィットネス関数:
  「このアーキテクチャの品質は維持されているか?」 → アーキテクチャテスト

依存性ルールのフィットネス関数

最も重要なフィットネス関数は「依存の方向が正しいか」を検証するものです。

// archunit風のテスト(TypeScript + カスタム検証)
describe('アーキテクチャ依存性ルール', () => {
  it('domain層はadapters層に依存しない', () => {
    const domainFiles = getAllFilesIn('src/domain/');
    for (const file of domainFiles) {
      const imports = getImports(file);
      const adapterImports = imports.filter(i => i.includes('/adapters/'));
      expect(adapterImports).toEqual([]);
    }
  });

  it('domain層はframeworks層に依存しない', () => {
    const domainFiles = getAllFilesIn('src/domain/');
    for (const file of domainFiles) {
      const imports = getImports(file);
      const fwImports = imports.filter(i =>
        i.includes('express') ||
        i.includes('@prisma') ||
        i.includes('stripe')
      );
      expect(fwImports).toEqual([]);
    }
  });

  it('use-cases層はadapters層に依存しない', () => {
    const useCaseFiles = getAllFilesIn('src/use-cases/');
    for (const file of useCaseFiles) {
      const imports = getImports(file);
      const adapterImports = imports.filter(i => i.includes('/adapters/'));
      expect(adapterImports).toEqual([]);
    }
  });
});

循環依存のフィットネス関数

describe('循環依存チェック', () => {
  it('モジュール間に循環依存がない', () => {
    const modules = ['domain', 'application', 'adapters', 'frameworks'];
    const dependencyGraph = buildDependencyGraph('src/');

    for (const module of modules) {
      const hasCycle = detectCycle(dependencyGraph, module);
      expect(hasCycle).toBe(false);
    }
  });
});

// ESLintルールとしても設定可能
// .eslintrc.js
module.exports = {
  rules: {
    'import/no-cycle': 'error', // 循環依存を検出
  },
};

パフォーマンスのフィットネス関数

describe('パフォーマンスフィットネス', () => {
  it('注文作成APIは200ms以内にレスポンスを返す', async () => {
    const start = Date.now();

    await request(app)
      .post('/api/orders')
      .send(sampleOrderPayload)
      .expect(201);

    const duration = Date.now() - start;
    expect(duration).toBeLessThan(200);
  });

  it('注文一覧APIは100件で500ms以内', async () => {
    // テストデータの投入
    await seedOrders(100);

    const start = Date.now();
    await request(app).get('/api/orders').expect(200);
    const duration = Date.now() - start;

    expect(duration).toBeLessThan(500);
  });
});

コード品質のフィットネス関数

describe('コード品質フィットネス', () => {
  it('EntityにORMデコレータが含まれない', () => {
    const entityFiles = getAllFilesIn('src/domain/entities/');
    for (const file of entityFiles) {
      const content = readFileSync(file, 'utf-8');
      expect(content).not.toContain('@Entity');
      expect(content).not.toContain('@Column');
      expect(content).not.toContain('from "typeorm"');
      expect(content).not.toContain('from "@prisma/client"');
    }
  });

  it('Aggregate Rootのみがpublic Repository Portを持つ', () => {
    const portFiles = getAllFilesIn('src/domain/ports/out/');
    for (const file of portFiles) {
      const content = readFileSync(file, 'utf-8');
      // Repository名がAggregate Root名と一致することを確認
      const repoName = extractInterfaceName(content);
      if (repoName?.endsWith('Repository')) {
        const entityName = repoName.replace('Repository', '');
        const entityExists = fileExists(`src/domain/entities/${entityName}.ts`);
        expect(entityExists).toBe(true);
      }
    }
  });
});

CI/CDパイプラインへの統合

# .github/workflows/architecture-fitness.yml
name: Architecture Fitness Check

on: [push, pull_request]

jobs:
  fitness:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci

      - name: 依存性ルールチェック
        run: npx jest --testPathPattern=fitness/dependency

      - name: 循環依存チェック
        run: npx madge --circular src/

      - name: パフォーマンステスト
        run: npx jest --testPathPattern=fitness/performance

      - name: コード品質チェック
        run: npx jest --testPathPattern=fitness/code-quality

フィットネス関数の分類

種別検証内容実行タイミング
静的依存性ルール、循環依存、コード品質CI(毎コミット)
動的パフォーマンス、レスポンス時間CI/CDステージング
運用可用性、エラー率、メモリ使用量本番監視

まとめ

ポイント内容
フィットネス関数アーキテクチャ品質の自動検証
依存性チェック最も重要 — 依存の方向が正しいか
循環依存チェックモジュール間の循環参照を検出
パフォーマンスレスポンス時間の回帰テスト
CI統合毎コミットで自動実行

チェックリスト

  • フィットネス関数の目的を説明できる
  • 依存性ルールのフィットネス関数を書ける
  • CI/CDへの統合方法を理解した
  • 静的・動的・運用の3分類を把握した

次のステップへ

次は「モジュラーモノリスという選択」を学びます。マイクロサービスの前段階として注目されるアーキテクチャパターンを理解しましょう。


推定読了時間: 30分