codebase-design について
小さなインターフェースの背後に多くの振る舞いを持つ、深いモジュールの設計を支援するスキルです。開発者向けに共通用語と原則を示し、変更の局所性やテスト容易性を高めます。
主な機能
- 01深いモジュールの設計指針
- 02インターフェースとシームの共通語彙
- 03アダプターとポートの選択原則
- 04依存性注入によるテスト容易性
- 05Goのtestifyアサーション指針
活用シーン
- 01明確なモジュール構成へのコード再編
- 02インターフェースの複雑さの評価
- 03シームにポートが必要かの判断
- 04実際の依存先を使ったテスト設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ lwlee2608/ agent-skills --skill codebase-designインストール後、Claude Code で/codebase-designを入力して読み込まれたか確認します。
同梱ファイルDEEPENING.mdDESIGN-IT-TWICE.md
詳細
コードベースの設計
深いモジュールを設計します。小さなインターフェースの背後に多くの振る舞いをまとめ、明確な継ぎ目に配置し、そのインターフェースを通じてテストできるようにします。コードを設計または再構成する際は、どこでもこの用語と原則を使ってください。目指すのは、呼び出し元にとっての活用度、保守担当者にとっての局所性、そしてすべての人にとってのテスト容易性です。
用語集
これらの用語を正確に使ってください。「コンポーネント」「サービス」「API」「境界」に置き換えてはいけません。一貫した用語を使うことが重要です。
モジュール:インターフェースと実装を備えたものすべてです。意図的に規模を問いません。関数、クラス、パッケージ、あるいは複数の層にまたがる一部分を指します。避ける用語:ユニット、コンポーネント、サービス。
インターフェース:モジュールを正しく使うために、呼び出し元が知っておく必要のあるすべてです。型シグネチャだけでなく、不変条件、順序の制約、エラーの種類、必要な設定、パフォーマンス特性も含みます。避ける用語:API、シグネチャ(狭すぎます。型レベルの表面だけを指します)。
実装:モジュールの内部、つまりコード本体です。アダプターとは異なります。小さなアダプターに大きな実装(Postgres リポジトリ)が備わっていることも、大きなアダプターに小さな実装(インメモリのフェイク)が備わっていることもあります。継ぎ目が話題の中心なら「アダプター」を、それ以外では「実装」を使ってください。
深さ:インターフェースを通じて得られる活用度です。呼び出し元(またはテスト)が、習得する必要のあるインターフェースの単位あたりに実行できる振る舞いの量を指します。多くの振る舞いが小さなインターフェースの背後にあるモジュールは深く、インターフェースが実装とほぼ同じくらい複雑なモジュールは浅くなります。
継ぎ目 (Michael Feathers):その場所を編集せずに振る舞いを変更できる場所です。モジュールのインターフェースが置かれる位置を指します。どこに継ぎ目を置くかは、継ぎ目の背後に何を置くかとは別の設計上の判断です。避ける用語:境界(DDD の境界づけられたコンテキストと意味が重なりすぎます)。
アダプター:継ぎ目にあるインターフェースを満たす具体的なものです。中身ではなく、果たす役割(どの枠を埋めるか)を表します。
ポート:アダプターを切り替えられるように、特に継ぎ目に設けられたインターフェースです(Ports & Adapters)。ポートとは、継ぎ目に少なくとも 2 つのアダプター(通常は本番用とテスト用)があることを想定したものです。ポートは言語レベルの interface である場合もありますが、すべての継ぎ目にポートが必要なわけではありません。
活用度:深さによって呼び出し元が得られるものです。習得するインターフェースの単位あたりに、より多くの機能を利用できます。1 つの実装が、N 個の呼び出し箇所と M 個のテストで効果を発揮します。
局所性:深さによって保守担当者が得られるものです。変更、バグ、知識、検証が呼び出し元全体に広がるのではなく、1 か所に集中します。一度修正すれば、どこでも修正済みになります。
深いモジュールと浅いモジュール
深いモジュール = 小さなインターフェース + 多くの実装:
┌──────────────────────┐
│ Small Interface │ ← Few methods, simple params
├──────────────────────┤
│ │
│ Deep Implementation │ ← Complex logic hidden
│ │
└──────────────────────┘
浅いモジュール = 大きなインターフェース + 少ない実装(避けてください):
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
インターフェースを設計するときは、次の点を確認してください。
- メソッドの数を減らせますか?
- パラメーターを簡素化できますか?
- 内部に隠せる複雑さを増やせますか?
原則
- 深さは実装ではなく、インターフェースの特性です。 深いモジュールは、内部で小さく、モック可能で、交換可能な部品を組み合わせていてもかまいません。それらはインターフェースの一部ではないというだけです。モジュールには、実装内で使われる(実装に対してプライベートな)内部の継ぎ目と、インターフェースにある外部の継ぎ目の両方を設けられます。
- 削除テスト。 モジュールを削除すると想像してください。複雑さがなくなるなら、それは単なる中継でした。N 個の呼び出し元に複雑さが再び現れるなら、そのモジュールには価値がありました。
- インターフェースがテストの境目です。 呼び出し元とテストは、同じ継ぎ目を通ります。インターフェースの先までテストしたいなら、そのモジュールの形が適切でない可能性があります。
- アダプターが 1 つなら、継ぎ目は仮定上のものです。アダプターが 2 つなら、実在する継ぎ目です。 その継ぎ目をまたいで実際に変化するものがない限り、継ぎ目を導入しないでください。実際の依存関係をテスト内で実行できない場合に限り、テストダブルを 2 つ目のアダプターとして数えます。
テスト容易性
テスト容易性は形状の結果であり、独立した目標ではありません。次の2つのルールによって実現します。
- 依存関係を受け取り、自分で作らない。 自身で依存関係を生成するモジュールは、すべての呼び出し元に対してそれを固定します。依存関係はパラメーターとして受け取ってください。
- 結果を返し、変更を加えない。 計算した内容を報告するモジュールなら、呼び出し元がそれを使って何をするか決められます。
モックできるようにするためだけにインターフェースを宣言しないでください。「これをモックする必要がある」は、ポートを設ける理由にはなりません。
Can the real dependency run inside the test?
yes -> use it. No interface.
(pure logic, in-memory state, temp files,
Postgres/Redis/Kafka in a container)
no -> port + two adapters, production and test.
(third-party APIs, another team's service
across a network seam)
依存関係を注入することで、モジュールをテスト可能にできます。その依存関係にインターフェースを宣言するかどうかは別の判断であり、「いいえ」を選ぶ場合にのみ宣言する価値があります。実際に実行できるものをモックしても、指定したとおりにコードが呼び出すことを証明するだけです。誤ったクエリ、誤ったエンドポイント、変化してしまったスキーマは検出できません。実際に起こるのは、こうした失敗です。
アサーション(Go)
手書きの if got != want { t.Fatalf(...) } ではなく、testify を使ってください。テストを続行できない場合(セットアップ、エラーなど)は require.*、続行できる場合は、1回の実行ですべての失敗を報告できるよう assert.* を使います。want を先に指定します。assert.Equal(t, want, got)。
関係性
- モジュールには、呼び出し元やテストに提示する窓口であるインターフェースが必ず1つあります。
- 深さはモジュールの特性であり、そのインターフェースを基準に測定します。
- シームとは、モジュールのインターフェースが存在する場所です。
- アダプターはシームに配置され、インターフェースを満たします。
- 深さは、呼び出し元に活用度を、保守担当者に局所性をもたらします。
採用しない捉え方
- 深さを実装行数とインターフェース行数の比率とする考え方(Ousterhout):実装の水増しを助長します。ここでは深さを活用度として捉えます。
- 「インターフェース」を言語のキーワード(Go や TypeScript の
interface)やクラスの公開メソッドとする考え方:範囲が狭すぎます。ここでいうインターフェースには、呼び出し元が把握しておく必要のあるすべての情報が含まれます。具体的な Go の構造体にも、すでにインターフェースがあります。Go のinterfaceを宣言するのはポートの場合に限ってください。アダプターが1つだけなら、追加されるのは窓口であり、深さではありません。 - 「境界」:DDD の境界付けられたコンテキストと意味が重なるため、シームまたはインターフェースと呼んでください。
- インターフェースを宣言する理由としてのモックのしやすさ:すべての依存関係にポートが必要であるかのように見せてしまいます。まず、実際の依存関係をテストで実行できるか確認してください。分類については DEEPENING.md を参照してください。
さらに深く理解する
- 依存関係があるクラスターを深める方法については、DEEPENING.md を参照してください。依存関係の分類、シームの規律、レイヤー化せず置き換える方法を説明しています。
- 代替インターフェースの検討については、DESIGN-IT-TWICE.md を参照してください。サブエージェントを並行して起動し、インターフェースを大きく異なる複数の方法で設計してから、深さ、局所性、シームの配置を基準に比較します。
クレジット
mattpocock/skills を基にしています。