mcpField
C
codebase-design

codebase-design

github.com/lwlee2608開発1142 回インストール今日更新
リポジトリ

codebase-design について

小さなインターフェースの背後に多くの振る舞いを持つ、深いモジュールの設計を支援するスキルです。開発者向けに共通用語と原則を示し、変更の局所性やテスト容易性を高めます。

主な機能

  1. 01深いモジュールの設計指針
  2. 02インターフェースとシームの共通語彙
  3. 03アダプターとポートの選択原則
  4. 04依存性注入によるテスト容易性
  5. 05Goのtestifyアサーション指針

活用シーン

  1. 01明確なモジュール構成へのコード再編
  2. 02インターフェースの複雑さの評価
  3. 03シームにポートが必要かの判断
  4. 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つのルールによって実現します。

  1. 依存関係を受け取り、自分で作らない。 自身で依存関係を生成するモジュールは、すべての呼び出し元に対してそれを固定します。依存関係はパラメーターとして受け取ってください。
  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 を基にしています。

コメント