本記事について
当サイトを閲覧いただきありがとうございます。 本記事はシリーズ『生成AI時代のアーキテクチャ超入門』の「アプリケーションアーキテクチャ」カテゴリ第1弾として、クラス設計について解説する記事です。
モジュール設計より一段内側、コードを書くレベルの話で日々の開発で最も頻繁に登場する設計判断です。本記事ではSOLID原則・継承vs委譲・テスタビリティ・デザインパターン・コード複雑度の数値Gate・AI時代の設計まで扱い、「このクラスが変更される理由は1つか」という根本の問いを中心に据えます。
このカテゴリの他の記事
「変更の波及範囲」を局所化する
優れたクラス設計は、機能追加や仕様変更が発生しても変更箇所が局所化されます。一方でクラス設計が崩壊しているコードベースでは、1つの機能を修正すると別の機能が壊れる、修正のたびに新しいバグが生まれる、という連鎖が起きます。クラス設計の質は「コードの寿命」を決めると言っても過言ではありません。
新人時代に書いた「とりあえずUserクラスに詰め込む」コードが、5年後に後任を苦しめる。という光景は業界のどこででも目撃できます。最初の1クラスの責任範囲をどう引くかが、将来の保守性を決めます。
クラス設計の技術を学ぶための共通言語がSOLID原則で、オブジェクト指向の土台となる5つのルールです。
SOLID原則
SOLID は Robert C. Martin(クリーンアーキテクチャの提唱者)が整理した、オブジェクト指向設計の5つの原則の頭文字を取ったものです。個別に見ると当たり前のことに見えますが、5つ全てを守ると「変更に強いコードになる」、という経験則です。
flowchart LR
S["S: Single Responsibility<br/>1クラス=1責任"]
O["O: Open/Closed<br/>拡張◎ 修正×"]
L["L: Liskov Substitution<br/>派生は基底と置換可"]
I["I: Interface Segregation<br/>細粒度IF"]
D["D: Dependency Inversion<br/>抽象に依存"]
GOAL([変更に強いコード])
S --> GOAL
O --> GOAL
L --> GOAL
I --> GOAL
D --> GOAL
classDef principle fill:#dbeafe,stroke:#2563eb;
classDef goal fill:#fef3c7,stroke:#d97706,stroke-width:2px;
class S,O,L,I,D principle;
class GOAL goal;
| 文字 | 原則 | 意味 |
|---|---|---|
| S | Single Responsibility | 単一責任。1クラスは1つの責務だけ |
| O | Open/Closed | 拡張に開き、修正に閉じる |
| L | Liskov Substitution | 派生クラスは基底クラスと置換可能 |
| I | Interface Segregation | インターフェースは細かく分ける |
| D | Dependency Inversion | 依存性逆転。抽象に依存する |
S: 単一責任の原則
1クラスは1つの責任だけを持つ。言い換えると「このクラスが変更される理由は1つだけ」という状態を保ちます。認証・プロフィール更新・メール送信を全て抱える UserService のようなクラスは、認証仕様が変わっても、プロフィール仕様が変わっても、メール仕様が変わっても修正が必要になり、変更のたびに他の責務を壊すリスクを抱えます。
| ❌ 悪い例 | ✅ 良い例 |
|---|---|
| UserService が 認証・プロフィール・メール全部 | AuthService / ProfileService / NotificationService に分割 |
責任の数だけ変更の軸が増えます。責任を1つに絞ると、各クラスは小さく、読みやすく、テストしやすくなります。
O: 開放閉鎖の原則
拡張に対して開いて、修正に対して閉じる。新しい機能を追加するときに既存コードを修正せずに済む状態を目指します。支払い手段を switch (method) で分岐する設計は、新しい支払い手段を追加するたびに全ての switch を修正することになり、バグが入り込む典型パターン。
代わりに PaymentMethod インターフェースを定義しておき、新しい支払い手段はそのインターフェースを実装するクラスとして追加するだけ、という設計にすれば、既存コードは一切触らずに機能が増やせます。ポリモーフィズムとインターフェースを使うと、この原則は自然と実現されます。
L: リスコフの置換原則
派生クラスは基底クラスと置き換え可能であるべきです。基底クラス Bird に fly() メソッドがあるのに、Penguin クラスが継承して fly() で例外を投げるような設計は、Bird を期待する呼び出し側で Penguin を渡すと壊れるため、リスコフ原則に違反しています。
これを避けるには、継承関係を見直すか、is-a(である)ではなく has-a(を持つ)で設計する(Composition / 委譲)のが定石です。現代のベストプラクティスは「継承より委譲」(Composition over Inheritance)。継承は使いどころを慎重に選びます。
I: インターフェース分離の原則
クライアントは使わないメソッドに依存すべきではありません。太いインターフェースは、使わないメソッドの仕様変更でも影響を受ける危険があるため、用途別に小さく分けるのが原則です。
IWorker { work(); eat(); sleep() } のように「働く・食べる・寝る」が1つのインターフェースにまとまっていると、ロボットを表現するクラスに eat() や sleep() を強制することになる。これを IWorkable / IEatable / ISleepable のように分割しておけば、必要な能力だけを実装すれば済みます。
D: 依存性逆転の原則
上位モジュールは下位モジュールに依存してはならない。どちらも抽象に依存します。クリーンアーキテクチャの肝となる原則で、「OrderService(業務ロジック)が MySQLUserRepository(DB実装)に依存する」という自然な依存方向を、逆転させるのがポイントです。
具体的には、OrderService の側が IUserRepository というインターフェースを定義し、MySQLUserRepository がそれを実装する形にします。これにより、業務ロジックがDB実装を知らない状態を作り出せる。DBを差し替えたくなった時、テストでモックを使いたい時、全て抽象の付け替えで対応できます。
実装手段としては、DI(Dependency Injection、依存をコンストラクタやセッターで外から注入する)と、インターフェースを業務ロジック層に置く(クリーンアーキテクチャの特徴)の2つが基本です。
「上位が下位に依存する」を「下位が上位のインターフェースに依存する」に反転させるから Dependency Inversion と呼びます。
継承よりも委譲
現代のオブジェクト指向設計では、継承よりも委譲(Composition)を優先するのがベストプラクティスとされています。継承は強い結びつき(is-a 関係)を作り出すため、基底クラスの変更が全派生クラスに波及しやすく、LSP違反の罠も招きやすいからです。
| 方式 | 特徴 |
|---|---|
| 継承 | 強結合・LSP違反の罠・多重継承の問題・テスト困難 |
| 委譲(Composition) | 疎結合・テスト容易・実行時差替可能・原則的に柔軟 |
❌ class Admin extends User // 継承(強結合)
✅ class Admin { private user: User } // 委譲(疎結合)
継承より委譲が原則です。継承は「本当に同じものの亜種」の場合に限定し、それ以外は委譲のほうが柔軟です。
テスタビリティ設計
テストしやすい設計=良い設計です。テストが書きにくいクラスは、依存が多すぎる・責任が曖昧・副作用が隠れている等の問題を抱えています。以下の原則を守るだけで、テスト容易性は格段に上がります。
- 依存はコンストラクタで受ける(DI):new で直接生成せず、外から注入する
- グローバル状態を避ける:シングルトンやstatic変数は最小限に
- 副作用と純粋ロジックを分離:計算ロジックは純粋関数、I/Oは薄い層に閉じ込める
- I/Oを薄い層に閉じ込める:DB・ファイル・外部APIは境界クラスだけが触る
❌ UserService が DB / Email / Redis を new する
✅ 依存を外から注入、テストでモック可能
よく使うパターン(Design Patterns)
頻繁に登場する設計パターンは、名前を知っておくとチーム内のコミュニケーションコストが下がります。ただしパターンを当てはめること自体が目的化すると、「ただクラスが増えるだけの過剰設計」になるので注意が必要です。
| パターン | 用途 |
|---|---|
| Repository | データアクセスを抽象化する |
| Factory | 複雑な生成ロジックを隠蔽する |
| Strategy | 振る舞いを差し替え可能にする |
| Adapter | 既存クラスのインターフェースを変換する |
| Observer | イベント通知・Pub/Sub(発行/購読型のメッセージ連携) |
| Decorator | 機能を積み重ねる |
パターンは目的達成の手段です。名前を当てはめるのが目的になると失敗します。
オブジェクト指向の落とし穴
SOLIDを知っていても、実装段階で以下のアンチパターンに陥ることは頻繁にあります。特に「神クラス」と「貧血ドメインモデル」は、クラス設計が崩壊した時に最もよく見かけるパターンです。
- 神クラス(God Class):何でも知っている巨大なクラス。単一責任に違反
- 貧血ドメインモデル:データだけ持ち、ロジックはService層にある。オブジェクト指向の外形だけ
- 過剰な継承ツリー:4層以上の継承は見直す。設計が複雑化しすぎているサイン
- privateだらけでテスト不能:テストしにくい=設計が悪いサイン
- Utilクラス症候群:静的メソッドの寄せ集め。関数の集積所になり、どこに何があるか分からなくなる
依存関係の可視化
クラス設計の健康状態を客観的に把握するには、依存関係の可視化が有効です。importや依存グラフを解析するツールを使うと、意図しない依存関係や循環依存を自動で検出できます。
| 言語 | ツール |
|---|---|
| Node.js / TypeScript | Dependency Cruiser / madge |
| Java / Kotlin | Archunit |
| Python | import-linter |
| Go | go-cleanarch |
循環依存は設計崩壊のサインです。A→B→C→Aのような依存を発見したら、責任の分離を見直します。
コード複雑度の数値Gate
※ 2026年4月時点の業界相場値です。テクノロジー・人材市場の変化で陳腐化するため、定期的にアップデートが必要です。
「良いクラス」を曖昧に判断すると崩壊するので、静的解析ツール(ESLint / SonarQube / Ruff 等)で数値で縛るのが現代の標準です。以下は業界で採用される定番値です。
| 指標 | 閾値 | 超えたらどうするか |
|---|---|---|
| 1ファイルの行数 | 300行 | 分割を検討 |
| 1メソッドの行数 | 50行 | 抽出メソッドで分割 |
| 1クラスのパブリックメソッド数 | 10個 | 責任が2つ以上混在している |
| 循環的複雑度(Cyclomatic Complexity) | 10 | if / switch を減らす・ポリモーフィズムで置き換え |
| ネストの深さ | 3段 | 早期return・ガード節で平坦化 |
| メソッドの引数 | 3個 | 4個以上はパラメータオブジェクト |
| クラス間の依存数 | 5個 | Fan-outが多すぎる場合は責任分割 |
| コピペの検出 | 5行以上の重複 | DRY原則で抽出 |
これらは SonarQube・CodeClimate・ESLint が標準で検出でき、「PR時にCIで自動ブロック」するのが現代流です。「後で直す」で放置したコードは必ず負債化するため、閾値超過は即その場で分割するのが鉄則です。
数値GateはPR時の自動チェックで運用します。人間の目視レビューに頼ってはいけません。
クラス設計の鬼門・禁じ手
SOLIDを知っていても、実装段階で事故る典型パターンを整理します。どれもコードの寿命を縮めるアンチパターンの常連です。
| 禁じ手 | なぜダメか |
|---|---|
| 神クラス(God Class)を育てる | 「ついでにここに書いちゃおう」の積み重ねで3000行級に。後から分割は実質書き直し |
| 貧血ドメインモデル | Entityがデータだけ持ち、ロジックが全部Service層。DDDの形だけ真似た失敗の定番 |
| 継承4段以上の深いツリー | LSP違反・変更の波及・理解困難の三重苦。継承より委譲が原則 |
Util / Helper / Manager という名前のクラス | 責任を語らない名前は、何でも詰め込める空箱。責任が明確なら具体名がつく |
static メソッドの寄せ集め | テスト不能・依存注入不可・モック不可。Utility Classは避ける |
| 循環依存(A→B→A) | 1クラスの修正が他クラスに連鎖。import/no-cycleで自動検出 |
コンストラクタで new する(DIなし) | テストでモック不能。依存は外から注入する |
private メソッドをテスト経由でカバーする設計 | テストしにくい=設計が悪いサイン。public API で戦う |
| ORM生成型(Entity)を全層で共有 | DBスキーマ変更がUI層まで波及。境界でDTO変換するのが鉄則 |
Fat Service / 貧血ドメイン / 神クラスは「DDDを採用した」と言っているプロジェクトの95%で見かけるアンチパターンです。パターン名ではなく「ロジックがどこに宿っているか」を問うのが肝要です。
Util / Manager / Helper と名付けた瞬間に責任の放棄。具体名がつかないなら設計が間違っています。
AI時代の視点
AI駆動開発が前提になると、クラス設計は「AIが生成したコードの読みやすさ・置き換えやすさ」が競争力の源泉になります。AIはクラスを瞬時に量産できますが、SOLID原則から外れるとすぐ神クラス・密結合を生成する。人間が設計原則で縛ることで、AI生成コードの品質が安定します。
| AI時代に有利 | AI時代に不利 |
|---|---|
| 単一責任の小さなクラス・関数 | 巨大な神クラス・長い関数 |
| コンストラクタDI・インターフェース経由の依存 | new直書き・グローバル状態 |
| 型・インターフェースで契約を明示 | 暗黙の規約・口頭伝承 |
| 継承の浅いツリー・委譲中心 | 深い継承階層 |
AIは「依存が多すぎるクラス」「責任が不明確なクラス」を生成しがちで、特に最初の1プロンプトで巨大な UserService のようなクラスが出てきます。責任範囲をプロンプトで明示し、「認証だけ」「プロフィールだけ」と分割を強制するのがAI活用のコツ。テスト可能性の高い設計(DI・純粋関数)は、AIがリファクタリング提案を出しやすい領域でもあります。
よくある勘違い
- 「SOLIDを守れば良いコードになる」 → 守ること自体を目的化するとクラス爆発を招きます。「このクラスが変更される理由は1つか」という問いに還元して使う
- 「継承は再利用のための仕組み」 → 継承は強い結合を作る。再利用目的なら委譲のほうが柔軟。「本当に同じものの亜種か」を問う
- 「デザインパターンを知っていれば良い設計ができる」 → パターンは道具箱です。問題を理解せずパターンを当てると、ただクラスが増えるだけの過剰設計になる
- 「テストは設計と関係ない後工程」 → テストが書きにくい=設計が悪いサイン。テスタビリティはクラス設計の質そのもの
3,000行の UserService(業界事例)
引き継いだ案件で、認証・課金・プロフィール・メール送信・通知まで全てを抱えた3,000行越えの UserService が存在した、という事例があります。メール文面を1行直しただけで課金のテストが落ちるという、「触ると壊れる状態」に陥っていて、機能追加のたびに「誰がこのボタンを押すと何が起こるか」の調査だけで半日が消えていた、という話が聞かれます。
似た案件を引き継いで、最初にやることが「このクラスが何をしているかを付箋に書き出す」で、その付箋が20枚を超えた時点で手で分割することは諦めた、という体験談もよく聞きます。神クラスは1日で生まれるものではなく、「ついでにここに書いちゃおう」の積み重ねで育ちます。
SOLIDの「S」を1回でも崩した時点でこの道は始まっていて、後から分割するには全機能の挙動を把握して書き直すしかない。クラス設計は「最初の1クラス」の責任範囲の引き方が勝負です。
神クラスを育ててしまったら、分割は書き直しに近い大工事です。最初に責任を1つに絞ることが死活的に重要です。
決めるべきこと — あなたのプロジェクトでの答えは?
以下の項目について、あなたのプロジェクトの答えを1〜2文で言語化してみてください。曖昧なまま着手すると、必ず後から「なぜそう決めたんだっけ」が問われます。
- クラス粒度の指針(単一責任をどこまで厳密に適用するか)
- 依存注入の方式(コンストラクタ / DIコンテナ)
- 継承 vs 委譲のデフォルト方針
- インターフェースの所有層(クリーン採用の場合)
- パッケージ・名前空間の切り方
- テストの粒度とカバレッジ目標
よくある失敗
- SOLIDの名前だけ振り回す ― チームで理解度が揃っていないと、かえって議論が空転する
- パターン適用が目的化 ― Strategyパターンで済む場面にAbstract Factoryを入れる等、過剰設計で保守性が逆に落ちる
- 継承を使いすぎる ― 「再利用したい」という理由だけで継承すると、あとで縛りがきつくなる。委譲を優先する
- テストを後回し ― テスタビリティを意識せずに書いたコードは、テストを追加しようとした時に書き直しになる
最終的な判断の仕方
クラス設計は「変更の波及範囲を局所化する技術」で、SOLIDはその共通言語です。5つの原則を暗記することに意味はなく、核心は「このクラスが変更される理由は1つか」という問いを常に自分に突きつけることにあります。
責任が2つ以上あるクラスは、片方の変更が他方を壊す連鎖を生み、コードの寿命を縮める。神クラス・貧血ドメイン・深い継承ツリーといったアンチパターンは全て「責任の混在」「抽象の欠如」「結合の強さ」という根っこを共有しており、SOLIDはそれに対処するための道具箱に過ぎません。
現代の決定的な軸は「AIに責任範囲を指示しやすい構造か」AIはコードを瞬時に量産しますが、放っておくと神クラス・密結合・深い継承を平気で生成する。「認証だけ」「プロフィールだけ」と責任をプロンプトで区切り、コンストラクタDI・インターフェース・純粋関数で依存を明示化すれば、AIはその制約内で正確に書いてくれる。
テスタビリティの高い設計(DI・副作用分離)はAIが理解しやすい形と完全に一致するため、「テストしやすい設計=AIが扱いやすい設計」という等式が成り立ちます。継承より委譲、深いツリーより浅く小さなクラス群を選ぶのが、AI時代の一貫した正解です。
選定の優先順位をまとめると次の通りです。
- 単一責任を徹底する(クラスが変更される理由は1つ)
- 継承より委譲をデフォルトにする(強結合を避ける)
- 依存注入 + 純粋関数で副作用を分離(テスト容易性=AI適性)
- パターンは手段と割り切る(当てはめるのが目的化しない)
まとめ
本記事はクラス設計について、SOLID原則・継承vs委譲・テスタビリティ・コード複雑度の数値Gateまで含めて解説しました。如何だったでしょうか。
「このクラスが変更される理由は1つか」を常に問い、継承より委譲、依存注入で副作用を分離する。これがAI時代も含めた2026年のクラス設計の現実解です。
次回はドメインロジック(Transaction Script vs DDD・Value Object・集約)について解説します。
シリーズ目次に戻る → 『生成AI時代のアーキテクチャ超入門』の歩き方
それでは次の記事も閲覧いただけると幸いです。
📚 シリーズ:生成AI時代のアーキテクチャ超入門(26/89)