コードベース設計
深いモジュールを設計する:小さなインターフェースの背後に多くの振る舞いを持ち、クリーンなシームに配置され、そのインターフェースを通じてテスト可能なもの。この言語と原則を、コードの設計または再構築が行われるあらゆる場所で使用する。目的は、呼び出し側にとってのレバレッジ、メンテナンス担当者にとっての局所性、そして全員にとってのテスト容易性である。
用語集
これらの用語を厳密に使用すること:「コンポーネント」「サービス」「API」「境界」などに言い換えない。一貫した言語がすべてである。
モジュール(Module):インターフェースと実装を持つもの。意図的にスケールに依存しない:関数、クラス、パッケージ、階層をまたがるスライス。避ける:ユニット、コンポーネント、サービス。
インターフェース(Interface):呼び出し側がモジュールを正しく使用するために知っておくべきすべて:型シグネチャだけでなく、不変条件、順序制約、エラーモード、必要な設定、パフォーマンス特性。避ける:API、シグネチャ(狭すぎる、型レベルの表面のみを指す)。
実装(Implementation):モジュールの内部、そのコードの本体。**アダプタ(Adapter)**とは区別する:アダプタは、大きな実装を持つ小さなアダプタ(PostgreSQLリポジトリ)や、小さな実装を持つ大きなアダプタ(インメモリフェイク)であり得る。シームが話題のときは「アダプタ」を使い、そうでなければ「実装」を使う。
深さ(Depth):インターフェースにおけるレバレッジ。呼び出し側(またはテスト)が学習すべきインターフェースの単位あたりに実行できる振る舞いの量。モジュールは、小さなインターフェースの背後に大量の振る舞いがあるときに深く、インターフェースが実装とほぼ同じくらい複雑なときに浅い。
シーム(Seam)(Michael Feathers): その場所で編集せずに振る舞いを変更できる場所。モジュールのインターフェースが存在する位置。シームをどこに置くかは、その背後に何を置くかとは別の設計上の決定である。避ける:境界(DDDの境界付けられたコンテキストと意味が重複する)。
アダプタ(Adapter):シームにおいてインターフェースを満たす具体的なもの。役割(どのスロットを埋めるか)を説明し、実体(内部に何があるか)は説明しない。
レバレッジ(Leverage):呼び出し側が深さから得るもの。学習したインターフェースの単位あたりの、より多くの機能。1つの実装がN個の呼び出しサイトとM個のテストで見返りをもたらす。
局所性(Locality):メンテナンス担当者が深さから得るもの。変更、バグ、知識、検証が呼び出し側に広がるのではなく、1か所に集中する。1回修正すれば、どこでも修正される。
深い vs 浅い
深いモジュール = 小さなインターフェース + 多くの実装:
┌─────────────────────┐
│ 小さなインターフェース │ ← 少ないメソッド、単純なパラメータ
├─────────────────────┤
│ │
│ 深い実装 │ ← 複雑なロジックを隠蔽
│ │
└─────────────────────┘
浅いモジュール = 大きなインターフェース + 少しの実装(避ける):
┌─────────────────────────────────┐
│ 大きなインターフェース │ ← 多くのメソッド、複雑なパラメータ
├─────────────────────────────────┤
│ 薄い実装 │ ← 単に通過させるだけ
└─────────────────────────────────┘
インターフェースを設計するときは、次のように問いかける:
- メソッドの数を減らせるか?
- パラメータを簡略化できるか?
- より多くの複雑さを内部に隠せるか?
原則
- 深さはインターフェースの特性であり、実装の特性ではない。 深いモジュールは内部的に、小さく、モック可能で、交換可能な部品で構成されうる。それらは単にインターフェースの一部ではない。モジュールは、内部シーム(実装にプライベートで、自身のテストで使用される)と、インターフェースにおける外部シームの両方を持つことができる。
- 削除テスト。 モジュールを削除することを想像せよ。複雑さが消えれば、それは単なるパススルーだった。複雑さがN個の呼び出し側に再出現すれば、それはそれだけの価値を発揮していた。
- インターフェースはテスト面である。 呼び出し側とテストは同じシームを越える。インターフェースを超えてテストしたければ、おそらくモジュールの形状が間違っている。
- 1つのアダプタは仮定のシームを意味する。2つのアダプタは本物のシームを意味する。 何かが実際にそのシームをまたいで変化しない限り、シームを導入してはならない。
テスト容易性のための設計
優れたインターフェースはテストを自然にする:
-
依存関係を受け入れ、作成しない。
// テスト可能 function processOrder(order, paymentGateway) {} // テスト困難 function processOrder(order) { const gateway = new StripeGateway(); } -
結果を返し、副作用を発生させない。
// テスト可能 function calculateDiscount(cart): Discount {} // テスト困難 function applyDiscount(cart): void { cart.total -= discount; } -
小さな表面積。 メソッドが少なければ、必要なテストも少ない。パラメータが少なければ、テストのセットアップも簡単。
関係
- モジュールは正確に1つのインターフェースを持つ(呼び出し側とテストに提示する表面)。
- 深さはモジュールの特性であり、そのインターフェースに対して測定される。
- シームはモジュールのインターフェースが存在する場所である。
- アダプタはシームに位置し、インターフェースを満たす。
- 深さは、呼び出し側にとってのレバレッジとメンテナンス担当者にとっての局所性を生み出す。
却下されたフレーミング
- 深さを実装行数とインターフェース行数の比率とする(Ousterhout):実装を水増しする動機になる。代わりに深さ=レバレッジを使用する。
- 「インターフェース」をTypeScriptの
interfaceキーワードやクラスのパブリックメソッドとする:狭すぎる。ここでのインターフェースには、呼び出し側が知っておくべきすべての事実が含まれる。 - 「境界」:DDDの境界付けられたコンテキストと意味が重複する。シームまたはインターフェースと言う。
さらに深く
- 依存関係を考慮したクラスターの深化、DEEPENING.md を参照:依存関係のカテゴリ、シームの規律、およびレイヤー化ではなく置き換えによるテスト。
- 代替インターフェースの探求、DESIGN-IT-TWICE.md を参照:並列サブエージェントを起動してインターフェースを根本的に異なる複数の方法で設計し、深さ、局所性、シーム配置で比較する。