CTS-KB

開発手法ガイド:SOLID 原則編 — AI 生成コードをレビューするチェックリスト

⏱ 約 8 分で読めます
#開発手法 #SOLID #AI駆動開発 #設計原則 #コードレビュー

🤖 AI 駆動開発で SOLID 原則が必要な理由

AI コーディングツールは 「動けば良い」コードを一瞬で書く 。ただ、その結果として SRP(単一責任)違反の God ClassOCP(開放閉鎖)違反の if/switch 積み重ね が量産される。

SOLID 原則は、AI 生成コードに対する 構造的な腐敗を検知するチェックリスト として機能する。人間がレビューするときの武器であり、AI に指示するときの共通語でもある。

AI 生成コードの典型的な問題対応する SOLID 原則
関連処理を 1 クラスに詰め込むSRP(単一責任)
新ケース追加で if/switch が増殖OCP(開放閉鎖)
継承による不整合LSP(リスコフ置換)
巨大インターフェースの強制実装ISP(インターフェース分離)
ドメインが ORM に直接依存DIP(依存性逆転)

本記事は開発手法ガイド:概要のシリーズ 3 本目。Robert C. Martin(Uncle Bob)が体系化した 5 原則を、AI 駆動開発のレビュー観点として使いこなす方法を整理します。

📜 5 原則の一言要約

名称一言で
SSingle Responsibility(単一責任)1 つのクラスに変更理由は 1 つだけ
OOpen/Closed(開放閉鎖)改修するな、拡張せよ
LLiskov Substitution(リスコフ置換)サブクラスは親クラスと置き換えても破綻しない
IInterface Segregation(インターフェース分離)使わないメソッドに依存させない
DDependency Inversion(依存性逆転)上位は下位ではなく抽象に依存する

💎 5 原則の本質は「疎結合」 — すべては同じ目的を共有している

5 原則は別々のアドバイスに見えますが、 すべて『疎結合』という同じ目的を達成するための手段 です。疎結合とは、 モジュール同士の結びつきを最小化し、片方を変えてももう片方が壊れない状態 を指します。

各原則が疎結合に寄与するメカニズム

原則どう疎結合を作るか
SRP変更理由を 1 つに絞る → 変更が他責務に波及しない
OCP拡張点を抽象化 → 新ケース追加で既存コードを触らない
LSP派生の契約を保つ → 親型に依存する側は実装を知らずに済む
ISP必要な操作だけを切り出す → 不要な依存を生まない
DIP上位は抽象に依存 → 下位を差し替えても上位は無傷

疎結合が崩れた瞬間に起きる連鎖

密結合

変更の影響が予測できない

テストが書けない(モック差し替え不能)

AI 生成コードを検証できない

動くか動かないかは「祈り」

AI 駆動開発の文脈では、 疎結合 = AI に任せられる範囲を構造で保証する仕組み です。

疎結合がもたらす AI 駆動開発の利得

利得仕組み
テストが書ける依存をモック・スタブ・インメモリ実装に差し替え可能
AI 生成コードを即座に検証できるテストで Red/Green を判定
変更影響を予測できる依存方向が一方向 → 波及範囲が見える
AI に局所的なタスクを任せられる影響が閉じる範囲が狭い

逆に 密結合のコードに AI を投入すると、生成速度に比例して破壊速度も上がります 。修正が他箇所を壊し、それを修正するとさらに別の箇所が壊れる無限ループに入ります。SOLID は AI 駆動開発を成立させるための 構造的な保険 として捉えてください。

🔍 各原則の典型違反と AI 生成コードでの現れ方

S — 単一責任原則(SRP)

原則: 1 つのクラスには、変更される理由が 1 つだけあるべき。

AI 生成コードでの現れ方: OrderService在庫引当・決済・通知・ログ出力 を全部抱える。「関連するから」という理由で AI がまとめる。

悪い例:
  class OrderService {
    placeOrder() {
      validateStock();        // 在庫
      chargeCard();           // 決済
      sendConfirmationEmail();// 通知
      writeAuditLog();        // ログ
    }
  }

是正:
  class PlaceOrderUseCase {
    constructor(
      private stock: StockService,
      private payment: PaymentService,
      private notifier: Notifier,
      private auditLog: AuditLogger,
    ) {}
    execute() { ... }
  }

レビュー質問: 「このクラスが変更される理由は何個ありますか?」 2 個以上なら分割を指示。

O — 開放閉鎖原則(OCP)

原則: 既存コードを修正せず、新機能を追加できるように設計する。

AI 生成コードでの現れ方: 商品種別・決済手段・モールが増えるたびに if/switch 文が肥大化

悪い例:
  function calculateShipping(order) {
    if (order.mall === 'yahoo')   { ... }
    else if (order.mall === 'rakuten') { ... }
    else if (order.mall === 'shopify') { ... }
    // Amazon 追加で修正が必要
  }

是正:
  interface ShippingCalculator { calculate(order): Price; }
  class YahooShipping implements ShippingCalculator { ... }
  class RakutenShipping implements ShippingCalculator { ... }
  // Amazon 追加は AmazonShipping を新規作成するだけ

レビュー質問: 「新しい種類が増えたとき、既存コードを触らず追加できますか?」

L — リスコフ置換原則(LSP)

原則: サブクラスは、親クラスと置き換えても動作が破綻してはならない。

AI 生成コードでの現れ方: 「似ているから継承しよう」で継承ツリーを深くする。古典的には Square extends RectanglesetWidth の意味が壊れる。

悪い例:
  class Rectangle { setWidth(w); setHeight(h); }
  class Square extends Rectangle {
    setWidth(w) { super.setWidth(w); super.setHeight(w); } // 高さも変わる
  }
  // Rectangle の契約「幅だけ変えたら高さは変わらない」を破る

是正:
  継承ではなく合成へ。あるいは Shape 抽象を設け、
  Rectangle と Square を同列に置く。

レビュー質問: 「親型の場所にこのサブクラスを入れても、呼び出し側は何も気にしなくていいですか?」

I — インターフェース分離原則(ISP)

原則: 使わないメソッドへの依存を強制してはならない。

AI 生成コードでの現れ方: 巨大インターフェース 1 つを実装側が空メソッドで埋める。

悪い例:
  interface FileHandler {
    read(); write(); compress(); encrypt(); upload();
  }
  class SimpleReader implements FileHandler {
    read() { ... }
    write() { throw new Error("not supported"); } // ISP 違反
  }

是正:
  interface Reader { read(); }
  interface Writer { write(); }
  interface Compressor { compress(); }
  // 必要な I/F だけ実装

レビュー質問: 「このインターフェースの全メソッドを、すべての実装が本当に使いますか?」

D — 依存性逆転原則(DIP)

原則: 上位モジュール(ビジネスルール)は下位モジュール(DB・外部 API)に依存せず、両者が抽象に依存するべき。

AI 生成コードでの現れ方: ドメイン層から直接 ORM(EntityFramework / Prisma / TypeORM)を呼ぶ。

悪い例:
  class Order {
    save() { dbContext.Orders.Add(this); dbContext.SaveChanges(); }
  }

是正:
  interface IOrderRepository { save(order); }  // ドメイン層に配置
  class PostgresOrderRepository implements IOrderRepository { ... } // インフラ層
  // Order はリポジトリを知らない

レビュー質問: 「ドメイン層のクラスが、具体的な DB / HTTP クライアント / ORM を直接触っていませんか?」

DIP の完全な適用が リポジトリパターンヘキサゴナルアーキテクチャ

🎯 AI への指示に SOLID を組み込む

指示テンプレート

タスク: {機能追加・変更内容}
禁止事項(SOLID 観点):
  - SRP: 既存の {Class} に別責務を追加しない
  - OCP: if/switch で新ケースを追加しない。戦略パターンで拡張
  - DIP: ドメイン層から {ORM / HTTP} を直接呼ばない

例:

タスク: 楽天モールへの出品機能を追加する。
禁止事項(SOLID 観点):
  - SRP: MallPublisher に楽天処理を追加しない。RakutenPublisher を新規作成
  - OCP: mall === 'rakuten' の if 分岐を作らない
  - DIP: 楽天 API の HttpClient をドメイン層から直接呼ばない

レビューチェックリスト

AI 生成コードを受け取ったときの確認順:

  1. SRP: クラスの変更理由が 1 つか(責務が絞られているか)
  2. OCP: 新しいケースが if/switch で追加されていないか
  3. LSP: 継承している場合、親の契約を破っていないか
  4. ISP: 実装側で空メソッド・NotSupported が発生していないか
  5. DIP: ドメイン層に具体的技術の import が混入していないか

⚠️ SOLID 原理主義のアンチパターン

SOLID を「守らねばならない戒律」として運用すると、かえって設計が歪む。

1. 過剰な抽象化

毎回 Interface + 1 つの実装 を量産する。将来の拡張のためと称して抽象を先回りで作る。

是正: 「今の時点で差し替え可能性があるか」 で判断。単一実装の Interface は不要。必要になってから抽象化。

2. SRP を理由にした過度な分割

「1 クラス 1 メソッド」レベルまで分割し、クラス数が爆発する。

是正: 「変更理由」で分ける。同じ理由で変わるものは同じクラスに。

3. DIP を理由にドメインに抽象を溢れさせる

全てに Interface を切り、ドメイン層がインターフェースの集合体になる。

是正: Interface を切るのは 境界(DB / 外部 API / メッセージング) だけ。ドメイン内部は具象で十分。

📚 まとめ

  • 5 原則の本質は すべて『疎結合』を作るための手段 。バラバラのアドバイスではなく、同じ目的を共有している
  • 疎結合 = AI に任せられる範囲を構造で保証する仕組み 。密結合に AI を投入すると破壊速度が生成速度を超える
  • SOLID は AI 生成コードの構造的腐敗を検知するチェックリスト として機能する
  • OCP は「改修するな、拡張せよ」 が最重要スローガン。if/switch の積み重ねは構造的腐敗のシグナル
  • SRP は「変更理由の数」、 OCP は「拡張で既存を触らないか」、 DIP は「依存方向が内向きか」で判定
  • AI への指示で「禁止事項(SOLID 観点)」を明示すると、生成コードの質が安定する
  • 原理主義は禁物。 境界で抽象化し、内部は具象 が実用的なバランス
  • SOLID は DDDリポジトリパターンヘキサゴナルアーキテクチャ を支える土台

🔗 関連記事

📖 関連用語