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