ストーリー
フィットネス関数とは
アーキテクチャフィットネス関数は、アーキテクチャの品質特性が維持されているかを自動検証する仕組みです。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分