🤖 AI 駆動開発で SOLID 原則が必要な理由
AI コーディングツールは 「動けば良い」コードを一瞬で書く 。ただ、その結果として SRP(単一責任)違反の God Class や OCP(開放閉鎖)違反の 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 原則の一言要約
| 略 | 名称 | 一言で |
|---|---|---|
| S | Single Responsibility(単一責任) | 1 つのクラスに変更理由は 1 つだけ |
| O | Open/Closed(開放閉鎖) | 改修するな、拡張せよ |
| L | Liskov Substitution(リスコフ置換) | サブクラスは親クラスと置き換えても破綻しない |
| I | Interface Segregation(インターフェース分離) | 使わないメソッドに依存させない |
| D | Dependency 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 Rectangle で setWidth の意味が壊れる。
悪い例:
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 生成コードを受け取ったときの確認順:
- SRP: クラスの変更理由が 1 つか(責務が絞られているか)
- OCP: 新しいケースが if/switch で追加されていないか
- LSP: 継承している場合、親の契約を破っていないか
- ISP: 実装側で空メソッド・NotSupported が発生していないか
- 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 ・ リポジトリパターン ・ ヘキサゴナルアーキテクチャ を支える土台
🔗 関連記事
- 開発手法ガイド:概要 — シリーズ全体の位置づけ
- 開発手法ガイド:オブジェクト指向編 — SOLID の前提となる責務分離
- 開発手法ガイド:DDD 編 — SOLID を業務ドメインに適用する
- COBOL × DDD 設計パターン — レガシー言語への SOLID 適用
📖 関連用語
- SOLID 原則 — 5 原則の要約
- オブジェクト指向 — SOLID が前提とするパラダイム
- DDD — SOLID を実装指針として活用する設計手法
- RDD(責務駆動設計) — SRP の出発点となる責務分析
- リポジトリパターン — DIP の代表的な適用例
- ヘキサゴナルアーキテクチャ — DIP をアーキテクチャ全体に適用した形
- クリーンアーキテクチャ — DIP を中核に据えた整理
- SoC(関心の分離) — SOLID の上位原則