アプリケーションアーキテクチャ

クラス設計の基礎 ― SOLID原則と継承vs委譲 ― 生成AI時代のアーキテクチャ超入門

クラス設計の基礎 ― SOLID原則と継承vs委譲 ― 生成AI時代のアーキテクチャ超入門

本記事について

当サイトを閲覧いただきありがとうございます。 本記事はシリーズ『生成AI時代のアーキテクチャ超入門』の「アプリケーションアーキテクチャ」カテゴリ第1弾として、クラス設計について解説する記事です。

モジュール設計より一段内側、コードを書くレベルの話で日々の開発で最も頻繁に登場する設計判断です。本記事ではSOLID原則・継承vs委譲・テスタビリティ・デザインパターン・コード複雑度の数値Gate・AI時代の設計まで扱い、「このクラスが変更される理由は1つか」という根本の問いを中心に据えます。

本記事のテーマについてさらに詳しく知りたい方は『アーキテクトの教科書』も参考にしてみてください。

そもそもクラス設計とは何か

クラス設計とは、ざっくり言えば「プログラムの部品(クラス)にどんな責任を持たせ、どう組み合わせるかを決めること」です。

レゴブロックを想像してください。1つのブロック(クラス)が小さく単純なら、組み替えも修理も簡単です。逆に巨大な一体成形パーツを作ると、一部が壊れただけで全体を捨てるしかありません。クラス設計も同じで、「1つのクラスに1つの責任」を徹底すると、変更時の影響範囲が小さくなり、コードの寿命が延びます。

なぜクラス設計が重要なのか

もしクラス設計を疎かにしたらどうなるか。1つの機能を修正すると別の機能が壊れる、修正のたびに新しいバグが生まれる、という連鎖が起きます。新人時代に書いた「とりあえずUserクラスに詰め込む」コードが、5年後に後任を苦しめる──という光景は業界のどこででも目撃できます。

クラス設計の質はコードの寿命を決めると言っても過言ではありません。その共通言語がSOLID原則で、オブジェクト指向の土台となる5つのルールです。

SOLID原則

SOLID原則の5要素

SOLID は Robert C. Martin(クリーンアーキテクチャの提唱者)が整理した、オブジェクト指向設計の5つの原則の頭文字を取ったものです。個別に見ると当たり前のことに見えますが、5つ全てを守ると「変更に強いコードになる」、という経験則です。

文字原則意味
SSingle Responsibility単一責任。1クラスは1つの責務だけ
OOpen/Closed拡張に開き、修正に閉じる
LLiskov Substitution派生クラスは基底クラスと置換可能
IInterface Segregationインターフェースは細かく分ける
DDependency Inversion依存性逆転。抽象に依存する

S: 単一責任の原則

1クラスは1つの責任だけを持つ。言い換えると「このクラスが変更される理由は1つだけ」という状態を保ちます。認証・プロフィール更新・メール送信を全て抱える UserService のようなクラスは、認証仕様が変わっても、プロフィール仕様が変わっても、メール仕様が変わっても修正が必要になり、変更のたびに他の責務を壊すリスクを抱えます。

❌ 悪い例✅ 良い例
UserService が 認証・プロフィール・メール全部AuthService / ProfileService / NotificationService に分割

責任の数だけ変更の軸が増えます。責任を1つに絞ると、各クラスは小さく、読みやすく、テストしやすくなります。

O: 開放閉鎖の原則

拡張に対して開いて、修正に対して閉じる。新しい機能を追加するときに既存コードを修正せずに済む状態を目指します。支払い手段を switch (method) で分岐する設計は、新しい支払い手段を追加するたびに全ての switch を修正することになり、バグが入り込む典型パターン

代わりに PaymentMethod インターフェースを定義しておき、新しい支払い手段はそのインターフェースを実装するクラスとして追加するだけ、という設計にすれば、既存コードは一切触らずに機能が増やせます。ポリモーフィズムとインターフェースを使うと、この原則は自然と実現されます。

L: リスコフの置換原則

派生クラスは基底クラスと置き換え可能であるべきです。基底クラス Birdfly() メソッドがあるのに、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 / TypeScriptDependency Cruiser / madge
Java / KotlinArchunit
Pythonimport-linter
Gogo-cleanarch

循環依存は設計崩壊のサインです。A→B→C→Aのような依存を発見したら、責任の分離を見直します。

コード複雑度の数値Gate

※ 2026年4月時点の業界相場値です。テクノロジー・人材市場の変化で陳腐化するため、定期的にアップデートが必要です。

「良いクラス」を曖昧に判断すると崩壊するので、静的解析ツール(ESLint / SonarQube / Ruff 等)で数値で縛るのが現代の標準です。以下は業界で採用される定番値です。

指標閾値超えたらどうするか
1ファイルの行数300行分割を検討
1メソッドの行数50行抽出メソッドで分割
1クラスのパブリックメソッド数10個責任が2つ以上混在している
循環的複雑度(Cyclomatic Complexity)10if / 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変換するのが鉄則
SOLIDの原則名だけ振り回すチームで理解度が揃っていないと議論が空転、原則の暗記ではなく責任の問いに還元する
デザインパターン適用を目的化Strategyで済む場面にAbstract Factoryを入れるなど、ただクラスが増える過剰設計になる

Fat Service / 貧血ドメイン / 神クラスはDDDを採用した」と言っているプロジェクトの95%で見かけるアンチパターンです。パターン名ではなく「ロジックがどこに宿っているか」を問うのが肝要です。

Util / Manager / Helper と名付けた瞬間に責任の放棄。具体名がつかないなら設計が間違っています。

AI判断軸

AI時代に有利AI時代に不利
単一責任の小さなクラス・関数巨大な神クラス・長い関数
コンストラクタDI・インターフェース経由の依存new直書き・グローバル状態
型・インターフェースで契約を明示暗黙の規約・口頭伝承
継承の浅いツリー・委譲中心深い継承階層

選定の優先順位をまとめると次の通りです。

  1. 単一責任を徹底する(クラスが変更される理由は1つ)
  2. 継承より委譲をデフォルトにする(強結合を避ける)
  3. 依存注入 + 純粋関数で副作用を分離(テスト容易性=AI適性)
  4. パターンは手段と割り切る(当てはめるのが目的化しない)

依存注入と純粋関数がAI生成コードのテスト容易性を保証する

依存注入(DI)で外部依存を注入可能にし、ビジネスロジックを純粋関数として書く設計は、AIが生成したコードのテストを容易にします。純粋関数は入出力が明確なのでAIがテストケースを自動生成しやすく、DIで外部依存を差し替えればIntegrationテストも書きやすくなります。

単一責任クラスはAIが正確に修正できる

1つのクラスが1つの責任だけを持つ設計であれば、AIに「このクラスのバリデーションロジックを修正して」と指示した際に影響範囲が限定されます。複数責務を持つGodクラスでは、AIが一部を修正した結果として別の責務が壊れるリスクがあります。

3,000行の UserService(業界事例)

引き継いだ案件で、認証・課金・プロフィール・メール送信・通知まで全てを抱えた3,000行越えの UserService が存在した、という事例があります。メール文面を1行直しただけで課金のテストが落ちるという、「触ると壊れる状態」に陥っていて、機能追加のたびに「誰がこのボタンを押すと何が起こるか」の調査だけで半日が消えていた、という話が聞かれます。

似た案件を引き継いで、最初にやることが「このクラスが何をしているかを付箋に書き出す」で、その付箋が20枚を超えた時点で手で分割することは諦めた、という体験談もよく聞きます。神クラスは1日で生まれるものではなく、「ついでにここに書いちゃおう」の積み重ねで育ちます。

SOLIDの「S」を1回でも崩した時点でこの道は始まっていて、後から分割するには全機能の挙動を把握して書き直すしかない。クラス設計は「最初の1クラス」の責任範囲の引き方が勝負です。

神クラスを育ててしまったら、分割は書き直しに近い大工事です。最初に責任を1つに絞ることが死活的に重要です。

決めるべきこと — 自分のプロジェクトでの答えは?

以下の項目について、自分のプロジェクトの答えを1〜2文で言語化してみてください。曖昧なまま着手すると、必ず後から「なぜそう決めたんだっけ」が問われます。

  • クラス粒度の指針(単一責任をどこまで厳密に適用するか)
  • 依存注入の方式(コンストラクタ / DIコンテナ)
  • 継承 vs 委譲のデフォルト方針
  • インターフェースの所有層(クリーン採用の場合)
  • パッケージ・名前空間の切り方
  • テストの粒度とカバレッジ目標

この記事に関連する記事

まとめ

本記事はクラス設計について、SOLID原則・継承vs委譲・テスタビリティ・コード複雑度の数値Gateまで含めて解説しました。如何だったでしょうか。

「このクラスが変更される理由は1つか」を常に問い、継承より委譲、依存注入で副作用を分離する。これがAI時代も含めた2026年のクラス設計の現実解です。

次回はドメインロジック(Transaction Script vs DDD・Value Object・集約)について解説します。

シリーズ目次に戻る → 『生成AI時代のアーキテクチャ超入門』の歩き方

本記事で扱った内容の詳細は Refactoring Guru - Design Patterns も合わせて参考にしてください。

それでは次の記事も閲覧いただけると幸いです。