本記事について
当サイトを閲覧いただきありがとうございます。 本記事はシリーズ『生成AI時代のアーキテクチャ超入門』の「アプリケーションアーキテクチャ」カテゴリ第1弾として、クラス設計について解説する記事です。
モジュール設計より一段内側、コードを書くレベルの話で日々の開発で最も頻繁に登場する設計判断です。本記事ではSOLID原則・継承vs委譲・テスタビリティ・コード複雑度の数値Gateまで扱い、「このクラスが変更される理由は1つか」という根本の問いを中心に据えます。
本記事のテーマについてさらに詳しく知りたい方は『アーキテクトの教科書』も参考にしてみてください。
この記事の結論
- 「このクラスが変更される理由は1つか」を常に問う(単一責任)
- 継承より委譲を基本にする
- 依存注入で副作用を分離し、テストできる形を保つ
- 複雑度は数値Gate(300行 / 50行 / 複雑度10)でCI強制する
この記事を読む前に
本記事はプログラムの書き方・整理の仕方の話が中心です。IT用語にあまり馴染みがない方は、基礎編の「プログラムとAPIの基本」を先に読んでおくと格段に分かりやすくなると思います。また、読んでいて分からない用語が出てきたときは用語集で調べながら読み進められます。
そもそもクラス設計とは何か
クラス設計とは、ざっくり言えば「プログラムの部品(クラス)にどんな責任を持たせ、どう組み合わせるかを決めること」です。
レゴブロックを想像してください。1つのブロック(クラス)が小さく単純なら、組み替えも修理も簡単です。逆に巨大な一体成形パーツを作ると、一部が壊れただけで全体を捨てるしかありません。クラス設計も同じで、「1つのクラスに1つの責任」を徹底すると、変更時の影響範囲が小さくなり、コードの寿命が延びます。
なぜクラス設計が重要なのか
もしクラス設計を疎かにしたらどうなるか。1つの機能を修正すると別の機能が壊れる、修正のたびに新しいバグが生まれる、という連鎖が起きます。新人時代に書いた「とりあえずUserクラスに詰め込む」コードが、5年後に後任を苦しめる──という光景は業界のどこででも目撃できます。
クラス設計の質はコードの寿命を決めると言っても過言ではありません。その共通言語がSOLID原則です。
SOLID原則
SOLIDはRobert C. Martinが整理した、オブジェクト指向設計の5つの原則の頭文字です。個別に見ると当たり前のことに見えますが、5つ全てを守ると「変更に強いコードになる」という経験則です。
| 文字 | 原則 | 意味 |
|---|---|---|
| S | Single Responsibility | 単一責任。1クラスは1つの責務だけ |
| O | Open/Closed | 拡張に開き、修正に閉じる |
| L | Liskov Substitution | 派生クラスは基底クラスと置換可能 |
| I | Interface Segregation | インターフェースは細かく分ける |
| D | Dependency Inversion | 依存性逆転。抽象に依存する |
S: 単一責任の原則
1クラスは1つの責任だけを持つ。言い換えると「このクラスが変更される理由は1つだけ」という状態を保ちます。認証・プロフィール更新・メール送信を全て抱える UserService は、どの仕様が変わっても修正が必要になり、変更のたびに他の責務を壊すリスクを抱えます。AuthService / ProfileService / NotificationServiceに分割すれば、各クラスは小さく、読みやすく、テストしやすくなります。
O: 開放閉鎖の原則
新しい機能を追加するときに既存コードを修正せずに済む状態を目指します。支払い手段を switch (method) で分岐する設計は、追加のたびに全てのswitchを修正することになりバグが入り込む典型です。PaymentMethod インターフェースを定義しておき、新しい支払い手段は実装クラスとして追加するだけにすれば、既存コードは一切触らずに機能が増やせます。
L: リスコフの置換原則
派生クラスは基底クラスと置き換え可能であるべきです。基底クラス Bird に fly() があるのに、継承した Penguin が fly() で例外を投げる設計は、Birdを期待する呼び出し側で壊れます。これを避けるには、is-a(である)ではなくhas-a(を持つ)で設計する──つまり「継承より委譲」(Composition over Inheritance)が定石です。
I: インターフェース分離の原則
クライアントは使わないメソッドに依存すべきではありません。「働く・食べる・寝る」が1つにまとまった太いインターフェースは、ロボットを表現するクラスに eat() を強制します。IWorkable / IEatable / ISleepable のように用途別に小さく分けておけば、必要な能力だけを実装すれば済みます。
D: 依存性逆転の原則
上位モジュールは下位モジュールに依存せず、どちらも抽象に依存します。OrderService(業務ロジック)の側が IUserRepository というインターフェースを定義し、MySQLUserRepository(DB実装)がそれを実装する形にすると、業務ロジックがDB実装を知らない状態を作れます。DBの差し替えもテストのモック化も、抽象の付け替えで対応できます。実装手段はDI(依存をコンストラクタで外から注入する)が基本です。
継承よりも委譲
現代のクラス設計では、継承よりも委譲(Composition)を優先するのがベストプラクティスです。継承は強い結びつき(is-a関係)を作り出すため、基底クラスの変更が全派生クラスに波及しやすく、LSP違反の罠も招きやすいからです。
❌ class Admin extends User // 継承(強結合)
✅ class Admin { private user: User } // 委譲(疎結合)
継承は「本当に同じものの亜種」の場合に限定し、それ以外は委譲のほうが疎結合でテストも容易、実行時の差し替えも可能です。
テスタビリティ設計
テストしやすい設計=良い設計です。テストが書きにくいクラスは、依存が多すぎる・責任が曖昧・副作用が隠れている等の問題を抱えています。原則は4つ、依存はコンストラクタで受ける(newで直接生成しない)、グローバル状態を避ける(シングルトン・static変数は最小限)、副作用と純粋ロジックを分離する(計算は純粋関数に)、I/Oを薄い層に閉じ込める(DB・外部APIは境界クラスだけが触る)です。
❌ UserService が DB / Email / Redis を new する
✅ 依存を外から注入、テストでモック可能
なおデザインパターン(Repository・Factory・Strategyが頻出3種、ほかにAdapter・Observer・Decorator等)は、名前を知っておくとチームのコミュニケーションコストが下がりますが、当てはめること自体が目的化するとただクラスが増えるだけの過剰設計になります。パターンは手段と割り切ります。
コード複雑度の数値Gate
※ 2026年4月時点の業界相場値です。
「良いクラス」を曖昧に判断すると崩壊するので、静的解析ツール(ESLint / SonarQube / Ruff等)で数値で縛るのが現代の標準です。
| 指標 | 閾値 | 超えたらどうするか |
|---|---|---|
| 1ファイルの行数 | 300行 | 分割を検討 |
| 1メソッドの行数 | 50行 | 抽出メソッドで分割 |
| 循環的複雑度 | 10 | if / switchを減らしポリモーフィズムへ |
| ネストの深さ | 3段 | 早期return・ガード節で平坦化 |
| メソッドの引数 | 3個 | 4個以上はパラメータオブジェクト |
これらはPR時にCIで自動ブロックするのが現代流です。「後で直す」で放置したコードは必ず負債化するため、閾値超過は即その場で分割するのが鉄則です。人間の目視レビューに頼ってはいけません。あわせて依存関係の可視化ツール(TypeScriptならDependency Cruiser、JavaならArchUnit等)で循環依存を自動検出すると、設計崩壊のサインを早期に掴めます。
3つのシナリオで考える
個人開発・スタートアップの場合
デザインパターンの適用は最小限にして、関数ベースと早期return、ESLintの複雑度チェックだけで十分だと思います。1人開発では凝ったクラス階層は認知負荷にしかなりません。数値Gate(1ファイル300行・複雑度10)の自動チェックさえ効かせておけば、規律としては足りてしまいます。
中小SaaSの場合
RepositoryとFactory、Strategyの頻出3パターンにDIコンテナを加えて、チームの共通言語として導入する段階です。テスト容易性(コンストラクタ注入でモックを差し替えられること)を設計基準にして、CIで複雑度の閾値をブロックする運用を確立していきます。
大企業の場合
数十人が同じコードベースを触ることになるため、依存関係の可視化(ArchUnitやDependency Cruiser)による層違反の自動検出まで踏み込む価値があります。パターンの語彙をチーム教育で揃えて、レビュー観点を標準化しておかないと、設計の一貫性が人数に負けてしまうのです。
AI判断軸 ― テスト容易性=AI適性
依存注入と純粋関数がAI生成コードの品質を保証する
DIで外部依存を注入可能にし、ビジネスロジックを純粋関数として書く設計は、AIが生成したコードのテストを容易にします。純粋関数は入出力が明確なのでAIがテストケースを自動生成しやすく、DIで外部依存を差し替えればIntegrationテストも書きやすくなります。テスタビリティの高い設計は、そのままAIと協働しやすい設計です。
単一責任クラスはAIが正確に修正できる
1つのクラスが1つの責任だけを持つ設計であれば、AIに「このクラスのバリデーションロジックを修正して」と指示した際に影響範囲が限定されます。複数責務を持つGodクラスでは、AIが一部を修正した結果として別の責務が壊れるリスクがあります。数値Gate(300行以内)はAIのコンテキストに収まる単位でもあり、AI時代にはガードレールとしての意味が増しています。
やってはいけないこと
SOLIDを知っていても実装段階で事故る典型を、特に危険な6つに絞ります。
| 禁じ手 | なぜダメか → どうするか |
|---|---|
| 神クラス(God Class)を育てる | 「ついでにここに」の積み重ねで3000行級になる → 責任1つを徹底し300行で分割する |
| 貧血ドメインモデル | Entityがデータだけ持ちロジックが全部Service層に溜まる → ロジックをEntity / 値オブジェクトに寄せる |
| 継承4段以上の深いツリー | LSP違反・変更の波及・理解困難の三重苦 → 委譲に組み替える |
Util / Helper / Manager という名前 | 責任を語らない名前は何でも詰め込める空箱 → 責任が明確なら具体名がつく |
コンストラクタで new する(DIなし) | テストでモック不能になる → 依存は外から注入する |
| 循環依存(A→B→A)を放置 | 1クラスの修正が連鎖する → import/no-cycle等で自動検出する |
Fat Service・貧血ドメイン・神クラスは「DDDを採用した」と言っているプロジェクトの多くで見かけるアンチパターンです。パターン名ではなく「ロジックがどこに宿っているか」を問うのが肝要です。
筆者メモ ― 3,000行の UserService
引き継いだ案件で、認証・課金・プロフィール・メール送信・通知まで全てを抱えた3,000行越えの UserService が存在した、という事例があります。メール文面を1行直しただけで課金のテストが落ちるという「触ると壊れる状態」に陥っていて、機能追加のたびに影響調査だけで半日が消えていたそうです。
神クラスは1日で生まれるものではなく、「ついでにここに書いちゃおう」の積み重ねで育ちます。後から分割するには全機能の挙動を把握して書き直すしかありません。クラス設計は「最初の1クラス」の責任範囲の引き方が勝負です。
決めるべきこと — 自分のプロジェクトでの答えは?
以下の項目について、自分のプロジェクトの答えを1〜2文で言語化してみてください。曖昧なまま着手すると、必ず後から「なぜそう決めたんだっけ」が問われます。
- クラス粒度の指針(単一責任をどこまで厳密に適用するか)
- 依存注入の方式(コンストラクタ / DIコンテナ)
- 継承 vs 委譲のデフォルト方針
- 数値Gateの閾値とCI強制の設定
- パッケージ・名前空間の切り方
- テストの粒度とカバレッジ目標
この記事に関連する記事
まとめ
本記事はクラス設計について、SOLID原則・継承vs委譲・テスタビリティ・コード複雑度の数値Gateまで含めて解説しました。如何だったでしょうか。
「このクラスが変更される理由は1つか」を常に問い、継承より委譲、依存注入で副作用を分離する。これがAI時代も含めた2026年のクラス設計の現実解です。
次回はドメインロジック(Transaction Script vs DDD・Value Object・集約)について解説します。
シリーズ目次に戻る → 『生成AI時代のアーキテクチャ超入門』の歩き方
本記事で扱った内容の詳細は Refactoring Guru - Design Patterns も合わせて参考にしてください。
それでは次の記事も閲覧いただけると幸いです。
📚 シリーズ:生成AI時代のアーキテクチャ超入門(32/95)
