Skilore

SOLID原則 ── オブジェクト指向設計の5大原則

SOLID原則は、変更に強く拡張しやすいソフトウェアを設計するための5つの基本原則である。Robert C. Martinが提唱し、Michael Feathersが頭文字をとって命名した。

85 分で読めます42,221 文字

SOLID原則 ── オブジェクト指向設計の5大原則

SOLID原則は、変更に強く拡張しやすいソフトウェアを設計するための5つの基本原則である。Robert C. Martinが提唱し、Michael Feathersが頭文字をとって命名した。


この章で学ぶこと

  1. SOLID各原則の意味と目的 ── SRP、OCP、LSP、ISP、DIPの本質を理解する
  2. 原則違反の兆候と影響 ── 違反が引き起こす設計上の問題を把握する
  3. 各原則の実践的な適用方法 ── 具体的なコード例で正しい設計を身につける
  4. 原則間の相互関係 ── 5つの原則がどのように連携し補完し合うかを理解する
  5. 適用の判断基準 ── 過度な適用を避け、実践的なバランス感覚を身につける

前提知識

前提知識 説明 参照リンク
オブジェクト指向プログラミング クラス、継承、ポリモーフィズム、インターフェース ../../02-programming/
クリーンコード概要 コード品質の基本概念と測定指標 00-clean-code-overview.md
抽象化の概念 抽象クラス、インターフェース、依存関係 ../../02-programming/

1. SOLID原則の全体像

1.1 各原則の概要

+------------------------------------------------------------------+
|                    SOLID 原則                                     |
+------------------------------------------------------------------+
| S - Single Responsibility Principle (単一責任の原則)              |
|     → クラスを変更する理由は1つだけにせよ                         |
+------------------------------------------------------------------+
| O - Open/Closed Principle (開放/閉鎖の原則)                       |
|     → 拡張に開き、修正に閉じよ                                   |
+------------------------------------------------------------------+
| L - Liskov Substitution Principle (リスコフの置換原則)            |
|     → 子クラスは親クラスと置換可能であれ                          |
+------------------------------------------------------------------+
| I - Interface Segregation Principle (インターフェース分離の原則)  |
|     → クライアントが使わないメソッドに依存させるな                |
+------------------------------------------------------------------+
| D - Dependency Inversion Principle (依存性逆転の原則)             |
|     → 抽象に依存し、具象に依存するな                              |
+------------------------------------------------------------------+

1.2 なぜSOLID原則が必要なのか ── WHYの深掘り

ソフトウェアは「最初に動くものを作る」だけなら比較的簡単だが、「変更し続けられるものを作る」のが困難である。SOLID原則が解決する根本的な問題は、変更の波及である。

変更の波及モデル

  SOLID原則なし                    SOLID原則あり
変更要求変更要求
vv
┌───────┐┌───────┐
ClassAClassA
└───┬───┘└───────┘
波及(変更はここで完結)
┌───┼───┐
v v vClassB, ClassC
B C D→ 影響なし
v v v
E F G
(6クラスに波及)(1クラスのみ変更)

SOLID原則の各原則が解決する具体的な問題:

原則 解決する問題 違反した場合の症状
SRP 1つの変更が無関係な機能に影響する 頻繁な予期しないバグ
OCP 新機能追加のたびに既存コードを修正する if/switch分岐の増殖
LSP 派生クラスが基底クラスの前提を破る instanceof チェックの増加
ISP 不要なメソッドへの依存を強制される 空のメソッド実装
DIP 具象クラスへの直接依存でテスト困難 モック作成が不可能

1.3 SOLID原則の歴史的背景

SOLID原則の各原則は、それぞれ異なる時代に異なる研究者により提唱された。

  タイムライン

  1988  Barbara Liskov  → LSP の原型論文
  1994  Liskov & Wing   → LSP の正式定義
  1996  Robert C. Martin → OCP, DIP を論文発表
  1988  Bertrand Meyer  → OCP の先駆的記述(Object-Oriented Software Construction 初版)
  2000  Robert C. Martin → SRP, ISP を体系化
  2004  Michael Feathers → 5原則を "SOLID" と命名
  2017  Robert C. Martin → Clean Architecture で SOLID を再定義

2. S ── 単一責任の原則 (SRP)

2.1 定義

「クラスを変更する理由は、たった1つだけであるべきだ」── Robert C. Martin

Robert C. Martin は後に定義を洗練させ、「変更理由」を「アクター」として再定義した:

「モジュールはたった1つのアクター(利害関係者)に対して責任を負うべきだ」── Clean Architecture (2017)

この再定義により、「変更理由」の曖昧さが解消された。アクターとは、そのコードの変更を要求しうる人やグループのことである。

変更理由が複数あるクラス        SRP適用後
EmployeeEmployee
─────────────────────────
calculatePay()──→getName()
generateReport()getDept()
アクター: 3つ             │ PayCalculator │
    ・CFO(給与計算ルール)     │  ──────────── │
    ・COO(レポート形式)       │  calculate()  │
    ・CTO(DB保存方法)         └──────────────┘
ReportGenerator
────────────
generate()
EmployeeRepo
────────────
save()
アクター: 各1つ

2.2 SRP違反の検出方法

SRP違反を検出するための実践的なチェックリスト:

  SRP違反チェックリスト

  □ クラス名に「And」「Or」「Manager」「Handler」が含まれる
  □ クラスの説明に「〜して、〜して、〜する」が必要
  □ import文が10個以上ある
  □ クラスが200行を超えている
  □ テスト時に無関係なモック/スタブが必要
  □ 異なるチーム/部門から変更要求が来る
  □ 変更のたびに無関係なテストが壊れる

2.3 コード例

コード例1: SRP違反と改善 ── ユーザー管理

# SRP違反: Userクラスが認証・永続化・通知すべてを担当
class User:
    def __init__(self, name: str, email: str, password: str):
        self.name = name
        self.email = email
        self.password = password
 
    def authenticate(self, password: str) -> bool:
        """認証ロジック(セキュリティチームが管理)"""
        return bcrypt.check(self.password, password)
 
    def save(self) -> None:
        """永続化ロジック(インフラチームが管理)"""
        db.execute("INSERT INTO users ...", self.name, self.email)
 
    def send_welcome_email(self) -> None:
        """通知ロジック(マーケティングチームが管理)"""
        smtp.send(self.email, "Welcome!", f"こんにちは {self.name}")
 
 
# SRP適用: 各責任を専用クラスに分離
class User:
    """ユーザーのドメインモデル(データ表現のみ)"""
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email
 
 
class AuthenticationService:
    """認証ロジックを担当(アクター: セキュリティチーム)"""
    def __init__(self, credential_store: "CredentialStore"):
        self.credential_store = credential_store
 
    def authenticate(self, user: User, password: str) -> bool:
        stored_hash = self.credential_store.get_hash(user.email)
        return bcrypt.check(stored_hash, password)
 
 
class UserRepository:
    """ユーザーの永続化を担当(アクター: インフラチーム)"""
    def __init__(self, db: "Database"):
        self.db = db
 
    def save(self, user: User) -> None:
        self.db.execute("INSERT INTO users ...", user.name, user.email)
 
    def find_by_email(self, email: str) -> User | None:
        row = self.db.query("SELECT * FROM users WHERE email = %s", email)
        return User(row['name'], row['email']) if row else None
 
 
class NotificationService:
    """通知送信を担当(アクター: マーケティングチーム)"""
    def __init__(self, mailer: "Mailer"):
        self.mailer = mailer
 
    def send_welcome(self, user: User) -> None:
        self.mailer.send(user.email, "Welcome!", f"こんにちは {user.name}")

コード例2: SRP適用の実践 ── ログ解析

# SRP違反: 1つのクラスがパース・フィルタ・集計・出力を担当
class LogAnalyzer:
    def analyze(self, log_file: str) -> None:
        # パース
        entries = []
        with open(log_file) as f:
            for line in f:
                parts = line.strip().split(' ')
                entries.append({
                    'timestamp': parts[0],
                    'level': parts[1],
                    'message': ' '.join(parts[2:])
                })
 
        # フィルタ
        errors = [e for e in entries if e['level'] == 'ERROR']
 
        # 集計
        counts = {}
        for error in errors:
            msg = error['message'][:50]
            counts[msg] = counts.get(msg, 0) + 1
 
        # 出力
        for msg, count in sorted(counts.items(), key=lambda x: -x[1]):
            print(f"{count:5d} | {msg}")
 
 
# SRP適用: 各責任を分離
from dataclasses import dataclass
from typing import Iterator
 
@dataclass
class LogEntry:
    timestamp: str
    level: str
    message: str
 
class LogParser:
    """ログファイルのパースを担当"""
    def parse(self, log_file: str) -> list[LogEntry]:
        entries = []
        with open(log_file) as f:
            for line in f:
                entries.append(self._parse_line(line))
        return entries
 
    def _parse_line(self, line: str) -> LogEntry:
        parts = line.strip().split(' ', 2)
        return LogEntry(
            timestamp=parts[0],
            level=parts[1],
            message=parts[2] if len(parts) > 2 else ''
        )
 
class LogFilter:
    """ログエントリのフィルタリングを担当"""
    def filter_by_level(
        self, entries: list[LogEntry], level: str
    ) -> list[LogEntry]:
        return [e for e in entries if e.level == level]
 
class LogAggregator:
    """ログの集計を担当"""
    def count_by_message(
        self, entries: list[LogEntry], prefix_length: int = 50
    ) -> dict[str, int]:
        counts: dict[str, int] = {}
        for entry in entries:
            key = entry.message[:prefix_length]
            counts[key] = counts.get(key, 0) + 1
        return counts
 
class LogReporter:
    """集計結果の出力を担当"""
    def print_summary(self, counts: dict[str, int]) -> None:
        for msg, count in sorted(counts.items(), key=lambda x: -x[1]):
            print(f"{count:5d} | {msg}")

3. O ── 開放/閉鎖の原則 (OCP)

3.1 定義

「ソフトウェアの構成要素は拡張に対して開かれ、修正に対して閉じていなければならない」── Bertrand Meyer

この原則の本質は、新しい振る舞いを追加する際に、既存のコードを変更しなくて済む設計を作ることにある。

3.2 OCPの実現手段

OCPを実現するための主要なパターンは3つある:

OCPの実現手段
1. ポリモーフィズム(最も一般的)
→ インターフェースを定義し、実装を差し替え可能にする
2. ストラテジーパターン
→ アルゴリズムをオブジェクトとして注入する
3. テンプレートメソッドパターン
→ 骨格をベースクラスに定義し、詳細を派生で実装

3.3 コード例

コード例3: OCP違反と改善 ── 図形の面積計算

// OCP違反: 新しい図形を追加するたびにこのクラスを修正する必要がある
class AreaCalculator {
  calculate(shape: any): number {
    if (shape.type === 'circle') {
      return Math.PI * shape.radius ** 2;
    } else if (shape.type === 'rectangle') {
      return shape.width * shape.height;
    } else if (shape.type === 'triangle') {
      return (shape.base * shape.height) / 2;
    }
    // 新しい図形を追加するたびに if 分岐が増える...
    throw new Error(`Unknown shape: ${shape.type}`);
  }
}
 
// OCP適用: 新しい図形はクラス追加のみで対応(既存コード変更不要)
interface Shape {
  area(): number;
  perimeter(): number;
}
 
class Circle implements Shape {
  constructor(private radius: number) {}
  area(): number {
    return Math.PI * this.radius ** 2;
  }
  perimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}
 
class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}
  area(): number {
    return this.width * this.height;
  }
  perimeter(): number {
    return 2 * (this.width + this.height);
  }
}
 
// 新しい図形の追加: 既存コードを一切変更しない
class Pentagon implements Shape {
  constructor(private side: number) {}
  area(): number {
    return (Math.sqrt(5 * (5 + 2 * Math.sqrt(5))) / 4) * this.side ** 2;
  }
  perimeter(): number {
    return 5 * this.side;
  }
}
 
class AreaCalculator {
  calculate(shape: Shape): number {
    return shape.area();  // 多態性で処理を委譲
  }
 
  calculateTotal(shapes: Shape[]): number {
    return shapes.reduce((total, shape) => total + shape.area(), 0);
  }
}

コード例4: OCP適用 ── 割引計算のストラテジーパターン

from abc import ABC, abstractmethod
from dataclasses import dataclass
from decimal import Decimal
 
@dataclass
class Order:
    subtotal: Decimal
    customer_type: str
    item_count: int
 
# OCP違反: 新しい割引ルールの追加には既存コードの修正が必要
class DiscountCalculatorBad:
    def calculate(self, order: Order) -> Decimal:
        if order.customer_type == 'vip':
            return order.subtotal * Decimal('0.20')
        elif order.customer_type == 'regular' and order.item_count >= 10:
            return order.subtotal * Decimal('0.10')
        elif order.customer_type == 'employee':
            return order.subtotal * Decimal('0.30')
        # 新しい割引ルール追加のたびに elif が増える
        return Decimal('0')
 
 
# OCP適用: ストラテジーパターンで拡張可能に
class DiscountStrategy(ABC):
    @abstractmethod
    def calculate(self, order: Order) -> Decimal:
        """割引額を計算する"""
        pass
 
    @abstractmethod
    def is_applicable(self, order: Order) -> bool:
        """この割引が適用可能か判定する"""
        pass
 
class VipDiscount(DiscountStrategy):
    def calculate(self, order: Order) -> Decimal:
        return order.subtotal * Decimal('0.20')
 
    def is_applicable(self, order: Order) -> bool:
        return order.customer_type == 'vip'
 
class BulkDiscount(DiscountStrategy):
    MIN_ITEMS = 10
    def calculate(self, order: Order) -> Decimal:
        return order.subtotal * Decimal('0.10')
 
    def is_applicable(self, order: Order) -> bool:
        return order.item_count >= self.MIN_ITEMS
 
class EmployeeDiscount(DiscountStrategy):
    def calculate(self, order: Order) -> Decimal:
        return order.subtotal * Decimal('0.30')
 
    def is_applicable(self, order: Order) -> bool:
        return order.customer_type == 'employee'
 
# 新しい割引を追加: SeasonalDiscount クラスを作るだけ
class SeasonalDiscount(DiscountStrategy):
    """季節限定割引(新規追加でも既存コード変更なし)"""
    def calculate(self, order: Order) -> Decimal:
        return order.subtotal * Decimal('0.15')
 
    def is_applicable(self, order: Order) -> bool:
        from datetime import date
        month = date.today().month
        return month in (7, 8, 12)  # 夏と年末
 
class DiscountCalculator:
    """割引計算のオーケストレーター(修正に閉じている)"""
    def __init__(self, strategies: list[DiscountStrategy]):
        self.strategies = strategies
 
    def calculate_best_discount(self, order: Order) -> Decimal:
        applicable = [
            s.calculate(order)
            for s in self.strategies
            if s.is_applicable(order)
        ]
        return max(applicable, default=Decimal('0'))
 
# 使用例: 戦略を注入
calculator = DiscountCalculator([
    VipDiscount(),
    BulkDiscount(),
    EmployeeDiscount(),
    SeasonalDiscount(),  # 新しい戦略を追加するだけ
])

4. L ── リスコフの置換原則 (LSP)

4.1 定義

「S が T の派生型であれば、プログラム中で T 型のオブジェクトを S 型のオブジェクトに置換しても、プログラムの性質は変わらない」── Barbara Liskov

4.2 LSPの契約モデル

LSP を正しく理解するには、「契約による設計(Design by Contract)」の概念が重要である。

契約モデル

  基底クラスが定義する契約:
事前条件 (Precondition)
→ メソッド呼び出し前に満たすべき条件
→ 派生クラスは事前条件を強化できない
事後条件 (Postcondition)
→ メソッド呼び出し後に保証される条件
→ 派生クラスは事後条件を弱化できない
不変条件 (Invariant)
→ オブジェクトが常に満たす条件
→ 派生クラスも維持しなければならない
違反の例:
  ・事前条件の強化: 基底は正の数を受け付けるが、派生は偶数のみ
  ・事後条件の弱化: 基底は非nullを返すが、派生はnullを返す場合がある
  ・不変条件の破壊: 基底はソート済みを保証するが、派生はしない

4.3 コード例

コード例5: LSP違反の典型例(Rectangle/Square問題)

class Rectangle:
    def __init__(self, width: int, height: int):
        self._width = width
        self._height = height
 
    @property
    def width(self) -> int:
        return self._width
 
    @width.setter
    def width(self, value: int):
        self._width = value
 
    @property
    def height(self) -> int:
        return self._height
 
    @height.setter
    def height(self, value: int):
        self._height = value
 
    def area(self) -> int:
        return self._width * self._height
 
 
# LSP違反: Square は Rectangle の契約を破る
class Square(Rectangle):
    def __init__(self, side: int):
        super().__init__(side, side)
 
    @Rectangle.width.setter
    def width(self, value: int):
        self._width = value
        self._height = value  # 幅を変えると高さも変わる!
 
    @Rectangle.height.setter
    def height(self, value: int):
        self._width = value
        self._height = value
 
 
# この関数は Rectangle の契約を前提としている
def test_area(rect: Rectangle):
    rect.width = 5
    rect.height = 4
    assert rect.area() == 20  # Square だと失敗!(5*5=25)
 
 
# LSP準拠: 共通インターフェースで設計
from abc import ABC, abstractmethod
 
class Shape(ABC):
    @abstractmethod
    def area(self) -> int:
        pass
 
    @abstractmethod
    def perimeter(self) -> int:
        pass
 
class Rectangle(Shape):
    def __init__(self, width: int, height: int):
        self._width = width
        self._height = height
 
    def area(self) -> int:
        return self._width * self._height
 
    def perimeter(self) -> int:
        return 2 * (self._width + self._height)
 
class Square(Shape):
    def __init__(self, side: int):
        self._side = side
 
    def area(self) -> int:
        return self._side ** 2
 
    def perimeter(self) -> int:
        return 4 * self._side

コード例6: LSP違反の実践的な例 ── コレクション

// LSP違反: ReadOnlyList が List の「追加可能」という契約を破る
class ReadOnlyList<T> extends ArrayList<T> {
    @Override
    public boolean add(T element) {
        throw new UnsupportedOperationException("読み取り専用です");
    }
 
    @Override
    public T remove(int index) {
        throw new UnsupportedOperationException("読み取り専用です");
    }
}
 
// List を受け取る関数は add() が使えることを前提としている
void addDefaultItems(List<String> list) {
    list.add("default1");  // ReadOnlyList だと実行時エラー!
    list.add("default2");
}
 
 
// LSP準拠: 適切なインターフェースを使い分ける
// Java の標準ライブラリは既にこの区別を提供している
void readItems(Iterable<String> items) {
    // 読み取りのみ → Iterable で十分
    for (String item : items) {
        System.out.println(item);
    }
}
 
void modifyItems(List<String> items) {
    // 変更が必要 → List を要求(ReadOnlyListは渡されない)
    items.add("new item");
}

4.4 LSP違反の検出パターン

検出パターン 対処法
instanceof チェック if (shape instanceof Circle) ポリモーフィズムに置換
UnsupportedOperationException throw new UnsupportedOperationException() インターフェース分離(ISP)
ダウンキャスト (Circle) shape 設計の見直し
条件分岐で型判定 if (type == "square") ストラテジーパターン
派生クラスで事前条件を強化 基底は正数、派生は正の偶数のみ 契約の再設計

5. I ── インターフェース分離の原則 (ISP)

5.1 定義

「クライアントは自分が利用しないメソッドに依存することを強制されるべきではない」── Robert C. Martin

5.2 ISPの内部メカニズム

ISP が解決する問題は「不必要な再コンパイル」と「不必要な再デプロイ」である。クライアントが使わないメソッドを含むインターフェースに依存すると、そのメソッドの変更時にクライアントも影響を受ける。

ISP違反: 太ったインターフェースの問題
FatInterface
─────────────
InterfaceAInterfaceBInterfaceC
methodA()methodB()methodC()
v              v              v
    ClientA       ClientB        ClientC

  methodB() の変更 → ClientB のみ再コンパイル

5.3 コード例

コード例7: ISP違反と改善 ── Worker インターフェース

// ISP違反: 巨大なインターフェース
interface Worker {
    void work();
    void eat();
    void sleep();
    void attendMeeting();
    void writeReport();
}
 
// ロボットはeat/sleepできないが、実装を強制される
class Robot implements Worker {
    public void work() { /* 作業する */ }
    public void eat() { throw new UnsupportedOperationException(); }   // LSP違反も!
    public void sleep() { throw new UnsupportedOperationException(); }
    public void attendMeeting() { throw new UnsupportedOperationException(); }
    public void writeReport() { throw new UnsupportedOperationException(); }
}
 
 
// ISP適用: 役割ごとにインターフェースを分離
interface Workable {
    void work();
}
 
interface Feedable {
    void eat();
}
 
interface Restable {
    void sleep();
}
 
interface Communicable {
    void attendMeeting();
    void writeReport();
}
 
// 人間: すべてを実装
class HumanWorker implements Workable, Feedable, Restable, Communicable {
    public void work() { /* 作業する */ }
    public void eat() { /* 食事する */ }
    public void sleep() { /* 睡眠する */ }
    public void attendMeeting() { /* 会議に出る */ }
    public void writeReport() { /* レポートを書く */ }
}
 
// ロボット: 必要なものだけ実装
class RobotWorker implements Workable {
    public void work() { /* 作業する */ }
}
 
// AIアシスタント: 作業とコミュニケーション
class AiAssistant implements Workable, Communicable {
    public void work() { /* 作業する */ }
    public void attendMeeting() { /* 議事録を取る */ }
    public void writeReport() { /* レポートを生成する */ }
}

コード例8: ISP適用 ── リポジトリインターフェース

// ISP違反: 全CRUD操作を1つのインターフェースに
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<void>;
  update(entity: T): Promise<void>;
  delete(id: string): Promise<void>;
  bulkInsert(entities: T[]): Promise<void>;
  executeRawQuery(sql: string): Promise<any>;
}
 
// 読み取り専用のレポートサービスでも全メソッドが見える
class ReportService {
  constructor(private repo: Repository<Order>) {}
  // save, delete, executeRawQuery は使わないのに依存している
}
 
 
// ISP適用: 用途別にインターフェースを分離
interface Readable<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
}
 
interface Writable<T> {
  save(entity: T): Promise<void>;
  update(entity: T): Promise<void>;
}
 
interface Deletable {
  delete(id: string): Promise<void>;
}
 
interface BulkOperable<T> {
  bulkInsert(entities: T[]): Promise<void>;
}
 
// 完全なCRUDリポジトリ
interface CrudRepository<T>
  extends Readable<T>, Writable<T>, Deletable {}
 
// レポートサービスは読み取り専用インターフェースのみに依存
class ReportService {
  constructor(private repo: Readable<Order>) {}
 
  async generateMonthlyReport(): Promise<Report> {
    const orders = await this.repo.findAll();
    // ... レポート生成ロジック
  }
}
 
// 管理画面は全機能を利用
class AdminService {
  constructor(private repo: CrudRepository<Order>) {}
 
  async deleteOrder(id: string): Promise<void> {
    await this.repo.delete(id);
  }
}

6. D ── 依存性逆転の原則 (DIP)

6.1 定義

「上位モジュールは下位モジュールに依存してはならない。両者とも抽象に依存すべきである」── Robert C. Martin

「抽象は詳細に依存してはならない。詳細が抽象に依存すべきである」

6.2 DIPの内部メカニズム

DIP は「依存関係の方向を逆転させる」ことで、上位のビジネスロジックを下位のインフラ詳細から独立させる。

DIP違反                        DIP適用
OrderSvcOrderSvc
│ 直接依存                     │ 抽象に依存
        v                             v
MySQLRepo<<interface>>
具象に直接依存              └───────┬────────┘
   → MySQLを変更すると              │ 実装
     OrderSvcも影響           ┌─────┼─────┐
                              v     v     v
                         MySQL  Postgres InMemory
                          Repo   Repo    Repo
   → どの実装に変えても OrderSvc は影響を受けない

6.3 コード例

コード例9: DIP違反と改善 ── 通知システム

# DIP違反: 上位モジュールが具象クラスに直接依存
class OrderService:
    def __init__(self):
        self.repository = MySQLOrderRepository()  # 具象への直接依存
        self.notifier = EmailNotifier()            # 具象への直接依存
        self.logger = FileLogger()                 # 具象への直接依存
 
    def place_order(self, order: "Order") -> None:
        self.repository.save(order)
        self.notifier.notify(order.customer, "注文を受け付けました")
        self.logger.log(f"注文 {order.id} を処理しました")
 
 
# DIP適用: 抽象(インターフェース)に依存
from abc import ABC, abstractmethod
 
class OrderRepository(ABC):
    @abstractmethod
    def save(self, order: "Order") -> None:
        pass
 
    @abstractmethod
    def find_by_id(self, order_id: str) -> "Order | None":
        pass
 
class Notifier(ABC):
    @abstractmethod
    def notify(self, recipient: str, message: str) -> None:
        pass
 
class Logger(ABC):
    @abstractmethod
    def log(self, message: str) -> None:
        pass
 
 
class OrderService:
    """上位モジュール: 抽象にのみ依存"""
    def __init__(
        self,
        repository: OrderRepository,
        notifier: Notifier,
        logger: Logger
    ):
        self.repository = repository
        self.notifier = notifier
        self.logger = logger
 
    def place_order(self, order: "Order") -> None:
        self.repository.save(order)
        self.notifier.notify(order.customer, "注文を受け付けました")
        self.logger.log(f"注文 {order.id} を処理しました")
 
 
# 下位モジュール: 抽象を実装
class PostgreSQLOrderRepository(OrderRepository):
    def __init__(self, connection_string: str):
        self.connection_string = connection_string
 
    def save(self, order: "Order") -> None:
        # PostgreSQL固有の実装
        pass
 
    def find_by_id(self, order_id: str) -> "Order | None":
        pass
 
class SlackNotifier(Notifier):
    def __init__(self, webhook_url: str):
        self.webhook_url = webhook_url
 
    def notify(self, recipient: str, message: str) -> None:
        # Slack API を使った通知
        pass
 
class CloudWatchLogger(Logger):
    def log(self, message: str) -> None:
        # AWS CloudWatch への送信
        pass
 
 
# 組み立て(Composition Root)
service = OrderService(
    repository=PostgreSQLOrderRepository("postgresql://..."),
    notifier=SlackNotifier("https://hooks.slack.com/..."),
    logger=CloudWatchLogger()
)
 
# テスト時: モックを注入
class MockRepository(OrderRepository):
    def __init__(self):
        self.saved_orders = []
 
    def save(self, order):
        self.saved_orders.append(order)
 
    def find_by_id(self, order_id):
        return next((o for o in self.saved_orders if o.id == order_id), None)
 
test_service = OrderService(
    repository=MockRepository(),
    notifier=MockNotifier(),
    logger=MockLogger()
)

コード例10: DIP と依存性注入(DI)の関係

// DIP はアーキテクチャ原則、DI はそれを実現する実装手法
 
// 1. コンストラクタインジェクション(最も推奨)
class UserService {
  constructor(
    private readonly repository: UserRepository,
    private readonly hasher: PasswordHasher,
    private readonly mailer: Mailer
  ) {}
 
  async register(email: string, password: string): Promise<User> {
    const hashedPassword = await this.hasher.hash(password);
    const user = new User(email, hashedPassword);
    await this.repository.save(user);
    await this.mailer.sendWelcome(email);
    return user;
  }
}
 
// 2. セッターインジェクション(オプショナルな依存に)
class ReportGenerator {
  private formatter: ReportFormatter = new DefaultFormatter();
 
  setFormatter(formatter: ReportFormatter): void {
    this.formatter = formatter;
  }
}
 
// 3. メソッドインジェクション(呼び出し毎に異なる依存)
class DataProcessor {
  process(data: RawData, transformer: DataTransformer): ProcessedData {
    return transformer.transform(data);
  }
}

7. SOLID原則の相互関係

7.1 関係図

SOLID原則の相互関係
┌─────┐ 前提条件 ┌─────┐
LSP─────────────────→OCP
└──┬──┘ └──┬──┘
型安全性実現手段
v v
┌─────┐ IF版 ┌─────┐
ISP←────────────────SRP
└──┬──┘ └─────┘
依存の最小化
v
┌─────┐
DIP← OCP を実現するための手段
└─────┘

7.2 関係の詳細

原則 主な焦点 他の原則との関係
SRP クラスの責任範囲 ISPのクラス版。凝集度を高める
OCP 拡張の柔軟性 DIPと組み合わせて多態性で実現。LSPが前提条件
LSP 継承の正しさ OCPの前提条件。型安全性を保証
ISP インターフェースの粒度 SRPのインターフェース版。DIPの依存を最小化
DIP 依存の方向 OCPを実現するための手段。ISPで依存を最小化

7.3 実践での組み合わせ

# SOLID原則の組み合わせ例: 通知サービス
 
# SRP: 通知送信の責任のみ
# OCP: 新しい通知チャネルはクラス追加で対応
# LSP: すべてのNotifierはsendメソッドの契約を守る
# ISP: 同期/非同期を分離
# DIP: 抽象に依存
 
from abc import ABC, abstractmethod
 
# ISP: 同期通知と非同期通知を分離
class SyncNotifier(ABC):
    @abstractmethod
    def send(self, recipient: str, message: str) -> bool:
        """同期的にメッセージを送信し、成否を返す"""
        pass
 
class AsyncNotifier(ABC):
    @abstractmethod
    async def send(self, recipient: str, message: str) -> str:
        """非同期でメッセージを送信し、ジョブIDを返す"""
        pass
 
# LSP: 各実装はインターフェースの契約を完全に守る
class EmailNotifier(SyncNotifier):
    """SRP: メール送信のみを担当"""
    def __init__(self, smtp_config: dict):
        self.smtp = SmtpClient(smtp_config)
 
    def send(self, recipient: str, message: str) -> bool:
        try:
            self.smtp.send_mail(recipient, message)
            return True
        except SmtpError:
            return False
 
class SlackNotifier(AsyncNotifier):
    """SRP: Slack通知のみを担当"""
    def __init__(self, webhook_url: str):
        self.webhook_url = webhook_url
 
    async def send(self, recipient: str, message: str) -> str:
        response = await http_post(self.webhook_url, {"text": message})
        return response["job_id"]
 
# OCP: 新しい通知チャネル追加時、既存コードを変更しない
class SmsNotifier(SyncNotifier):
    """新規追加: SMS通知"""
    def __init__(self, api_key: str):
        self.sms_client = SmsClient(api_key)
 
    def send(self, recipient: str, message: str) -> bool:
        return self.sms_client.send_sms(recipient, message)
 
# DIP: NotificationService は抽象にのみ依存
class NotificationService:
    def __init__(self, notifiers: list[SyncNotifier]):
        self.notifiers = notifiers
 
    def notify_all(self, recipient: str, message: str) -> dict[str, bool]:
        results = {}
        for notifier in self.notifiers:
            name = type(notifier).__name__
            results[name] = notifier.send(recipient, message)
        return results

8. 適用の判断基準

8.1 適用すべき場面と避けるべき場面

状況 SOLID適用 過度な適用を避ける
頻繁に変更される箇所 積極的に適用 --
安定したユーティリティ 最低限で十分 過度な抽象化はYAGNI違反
プロトタイプ/PoC 後回しでよい 設計に時間をかけすぎない
チーム開発のコアロジック 必須 --
1回限りのスクリプト 不要 オーバーエンジニアリング
ライブラリ/フレームワーク 必須 利用者の使いやすさも考慮
マイクロサービスの境界 必須(特にDIP) サービス内部は適宜

8.2 段階的適用のガイドライン

SOLID原則の段階的適用フロー

  Step 1: SRP から始める(最も直感的)
  ├── 巨大クラスを見つけたら分割
  └── 「この関数は何をするか1文で説明できるか?」

  Step 2: DIP を適用(テスト容易性の向上)
  ├── 外部サービス依存をインターフェースで抽象化
  └── コンストラクタインジェクションを導入

  Step 3: OCP を意識(変更が多い箇所)
  ├── if/switch の増殖を発見したらポリモーフィズム化
  └── ストラテジーパターンの適用

  Step 4: ISP で微調整
  ├── 太ったインターフェースを分離
  └── クライアントごとに必要最小限のインターフェース

  Step 5: LSP で品質保証
  ├── 継承関係の正当性を検証
  └── 契約テストの追加

8.3 過度な適用のコスト

  SOLID適用のコスト-ベネフィット曲線

  ベネフィット
    ^
    |        *****
    |    ****     ***
    |  **             **
    | *                 *         ← 適度な適用
    |*                   *
    |                     **      ← 過度な適用
    +-------------------------> SOLID適用度
    0%   25%   50%   75%  100%

    0-50%: 適用するほどベネフィット増大
    50-75%: ベネフィットは緩やかに増大
    75-100%: 抽象化のオーバーヘッドがベネフィットを上回る

9. SOLID原則と他のパラダイム

9.1 関数型プログラミングとSOLID

SOLID原則 関数型での対応概念 説明
SRP 純粋関数 各関数は1つの変換のみ
OCP 高階関数 関数を引数で受け取り動作を拡張
LSP 参照透過性 同じ入力には常に同じ出力
ISP 型クラス(Haskell) 必要な振る舞いのみを要求
DIP 関数の注入 具体的な関数ではなく関数型を受け取る
# 関数型でのOCP: 高階関数による拡張
from typing import Any, Callable
 
# ソート戦略を関数として注入(OCP + DIP)
def sort_users(
    users: list[dict],
    key_fn: Callable[[dict], Any] = lambda u: u['name']
) -> list[dict]:
    return sorted(users, key=key_fn)
 
# 新しいソート基準の追加: 既存コード変更なし
by_age = lambda u: u['age']
by_score_desc = lambda u: -u['score']
 
sort_users(users, key_fn=by_age)
sort_users(users, key_fn=by_score_desc)

10. アンチパターン

アンチパターン1: God Class(SRP違反の極致)

# NG: 1つのクラスに全責任を詰め込む
class Application:
    def authenticate_user(self): ...
    def process_payment(self): ...
    def generate_report(self): ...
    def send_notification(self): ...
    def validate_input(self): ...
    def manage_cache(self): ...
    def handle_logging(self): ...
    # 1000行以上のメソッドが続く...
 
# OK: 責任ごとにクラスを分割
class AuthService: ...
class PaymentService: ...
class ReportService: ...
class NotificationService: ...
class InputValidator: ...
class CacheManager: ...
class Logger: ...

アンチパターン2: 過度な抽象化(SOLID原理主義)

// NG: 1メソッドのためにインターフェース + 実装 + ファクトリ + DI設定
interface StringFormatter { String format(String s); }
class UpperCaseFormatter implements StringFormatter {
    public String format(String s) { return s.toUpperCase(); }
}
class StringFormatterFactory {
    public StringFormatter create(String type) { ... }
}
class StringFormatterConfig {
    @Bean
    public StringFormatter formatter() { return new UpperCaseFormatter(); }
}
// 実際にはただの s.toUpperCase() で十分
 
// OK: 必要性が生じたら抽象化する
String formatted = input.toUpperCase();

アンチパターン3: Leaky Abstraction(抽象の漏洩)

# NG: インターフェースがDBの詳細を漏洩
class UserRepository(ABC):
    @abstractmethod
    def find_by_sql(self, sql: str) -> list[User]:
        """SQLクエリでユーザーを検索する"""
        pass  # SQL前提 → RDB以外の実装で困る
 
    @abstractmethod
    def set_connection_pool_size(self, size: int) -> None:
        """接続プールサイズを設定する"""
        pass  # 接続プール前提 → インメモリ実装で無意味
 
# OK: ドメインの言葉でインターフェースを定義
class UserRepository(ABC):
    @abstractmethod
    def find_by_email(self, email: str) -> User | None:
        """メールアドレスでユーザーを検索する"""
        pass
 
    @abstractmethod
    def find_active_users(self, since: datetime) -> list[User]:
        """指定日以降にアクティブなユーザーを検索する"""
        pass

11. 実践演習

演習1(基礎): SRP違反の検出と修正

以下のクラスからSRP違反を特定し、責任を分離せよ。

class ReportManager:
    def __init__(self, db_connection):
        self.db = db_connection
 
    def fetch_sales_data(self, start_date, end_date):
        query = f"SELECT * FROM sales WHERE date BETWEEN '{start_date}' AND '{end_date}'"
        return self.db.execute(query)
 
    def calculate_totals(self, sales_data):
        total = sum(item['amount'] for item in sales_data)
        tax = total * 0.10
        return {'subtotal': total, 'tax': tax, 'total': total + tax}
 
    def format_as_html(self, report_data):
        html = "<html><body>"
        html += f"<h1>売上レポート</h1>"
        html += f"<p>合計: {report_data['total']}円</p>"
        html += "</body></html>"
        return html
 
    def send_email(self, recipient, html_content):
        import smtplib
        server = smtplib.SMTP('localhost')
        server.sendmail('reports@company.com', recipient, html_content)
        server.quit()
 
    def generate_and_send(self, start_date, end_date, recipient):
        data = self.fetch_sales_data(start_date, end_date)
        totals = self.calculate_totals(data)
        html = self.format_as_html(totals)
        self.send_email(recipient, html)

期待される分析:

責任の分離:

  • SalesDataRepository: データ取得(アクター: DBA/インフラチーム)
  • SalesCalculator: 集計計算(アクター: 経理部門)
  • HtmlReportFormatter: HTML整形(アクター: デザインチーム)
  • EmailSender: メール送信(アクター: インフラチーム)
  • ReportService: オーケストレーション(責任を持たない調整役)

期待される出力例:

class SalesDataRepository:
    def __init__(self, db):
        self.db = db
 
    def fetch(self, start_date: str, end_date: str) -> list[dict]:
        return self.db.execute(
            "SELECT * FROM sales WHERE date BETWEEN %s AND %s",
            (start_date, end_date)
        )
 
class SalesCalculator:
    TAX_RATE = Decimal('0.10')
 
    def calculate_totals(self, sales_data: list[dict]) -> dict:
        subtotal = sum(Decimal(str(item['amount'])) for item in sales_data)
        tax = subtotal * self.TAX_RATE
        return {'subtotal': subtotal, 'tax': tax, 'total': subtotal + tax}
 
class ReportFormatter(ABC):
    @abstractmethod
    def format(self, report_data: dict) -> str: ...
 
class HtmlReportFormatter(ReportFormatter):
    def format(self, report_data: dict) -> str:
        return f"""<html><body>
        <h1>売上レポート</h1>
        <p>合計: {report_data['total']}円</p>
        </body></html>"""
 
class EmailSender:
    def __init__(self, smtp_host: str, from_address: str):
        self.smtp_host = smtp_host
        self.from_address = from_address
 
    def send(self, recipient: str, content: str) -> None:
        # SMTP送信ロジック
        pass
 
class ReportService:
    def __init__(self, repo, calculator, formatter, sender):
        self.repo = repo
        self.calculator = calculator
        self.formatter = formatter
        self.sender = sender
 
    def generate_and_send(self, start_date, end_date, recipient):
        data = self.repo.fetch(start_date, end_date)
        totals = self.calculator.calculate_totals(data)
        content = self.formatter.format(totals)
        self.sender.send(recipient, content)

演習2(応用): OCP を適用した拡張設計

以下の決済処理クラスを、新しい決済方法の追加時に既存コードを変更しなくて済むように設計し直せ。

class PaymentProcessor:
    def process(self, payment_method: str, amount: float) -> bool:
        if payment_method == "credit_card":
            # クレジットカード決済ロジック
            return self._process_credit_card(amount)
        elif payment_method == "bank_transfer":
            # 銀行振込ロジック
            return self._process_bank_transfer(amount)
        elif payment_method == "paypal":
            # PayPalロジック
            return self._process_paypal(amount)
        else:
            raise ValueError(f"未対応の決済方法: {payment_method}")

期待される出力例:

from abc import ABC, abstractmethod
 
class PaymentMethod(ABC):
    @abstractmethod
    def process(self, amount: float) -> bool: ...
 
    @abstractmethod
    def name(self) -> str: ...
 
class CreditCardPayment(PaymentMethod):
    def process(self, amount: float) -> bool:
        # クレジットカード決済ロジック
        return True
    def name(self) -> str:
        return "credit_card"
 
class BankTransferPayment(PaymentMethod):
    def process(self, amount: float) -> bool:
        # 銀行振込ロジック
        return True
    def name(self) -> str:
        return "bank_transfer"
 
# 新しい決済方法の追加: 既存コード変更なし
class CryptoPayment(PaymentMethod):
    def process(self, amount: float) -> bool:
        # 暗号通貨決済ロジック
        return True
    def name(self) -> str:
        return "crypto"
 
class PaymentProcessor:
    def __init__(self):
        self._methods: dict[str, PaymentMethod] = {}
 
    def register(self, method: PaymentMethod) -> None:
        self._methods[method.name()] = method
 
    def process(self, method_name: str, amount: float) -> bool:
        if method_name not in self._methods:
            raise ValueError(f"未対応の決済方法: {method_name}")
        return self._methods[method_name].process(amount)

演習3(発展): SOLID原則を全面適用した設計

以下の要件を、SOLID原則に準拠して設計・実装せよ。

要件: 図書館の書籍管理システム

  • 書籍の登録・検索・貸出・返却
  • 貸出通知(メール/SMS)
  • 延滞チェックと罰金計算
  • 複数のデータストア対応(DB/ファイル/インメモリ)

期待される設計の概要:

SRP: 各クラスが単一の責任
  ├── Book (ドメインモデル)
  ├── BookRepository (永続化)
  ├── LoanService (貸出ビジネスロジック)
  ├── FineCalculator (罰金計算)
  ├── NotificationService (通知送信)
  └── LibraryFacade (オーケストレーション)

  OCP: 通知チャネルの追加にクラス追加のみ
  DIP: LoanService → BookRepository(抽象) に依存
  ISP: 検索用/管理用で別インターフェース
  LSP: すべてのRepository実装が契約を守る

12. FAQ

Q1: SOLID原則はすべて同時に適用すべきか?

すべてを一度に適用する必要はない。まずSRPから始め、変更が多い箇所にOCPとDIPを適用するのが実践的。プロジェクトの規模と変更頻度に応じて段階的に導入する。小規模なスクリプトやプロトタイプにSOLIDを完全適用するのはオーバーエンジニアリングである。

Q2: SOLIDは関数型プログラミングにも適用できるか?

概念的には適用可能。SRPは「関数は1つのことをする」、OCPは「高階関数で拡張する」、DIPは「関数の注入」に対応する。ただし用語はOOP文脈で定義されたものなので、関数型では別の原則名(純粋性、合成可能性、参照透過性など)で語られることが多い。

Q3: LSP違反をどうやって検出するか?

以下のコードスメルが検出のヒント:

  • 派生クラスで UnsupportedOperationException をスローしている
  • instanceof / typeof チェックが増えている
  • 基底クラスの事前条件を強化、事後条件を弱化している
  • 「is-a」関係が成り立たない継承がある(正方形 is-a 長方形の問題)

自動検出の方法として、基底クラスのテストスイートを派生クラスに対しても実行する「契約テスト」がある。

Q4: DIPとDIコンテナは必須か?

DIPは原則であり、DIコンテナはその実現手段の一つに過ぎない。コンストラクタインジェクションだけでもDIPは実現できる。DIコンテナが必要になるのは、依存グラフが複雑になった大規模アプリケーションの場合。小規模なプロジェクトではマニュアルDI(手動で依存を組み立てる)で十分。

Q5: SOLIDとマイクロサービスの関係は?

マイクロサービスアーキテクチャはSOLID原則の「サービスレベル」での適用と見ることができる:

  • SRP: 各サービスは1つのビジネスドメインに責任を持つ
  • OCP: 新機能は新サービスの追加で対応
  • LSP: サービスのAPIコントラクトを守る
  • ISP: 必要なAPIのみを公開する
  • DIP: サービス間はメッセージキュー等の抽象を介して通信

FAQ

Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?

実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。

Q2: 初心者がよく陥る間違いは何ですか?

基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。

Q3: 実務ではどのように活用されていますか?

このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。


まとめ

原則 一言で 違反の兆候 改善手法
SRP 変更理由は1つ 巨大クラス、頻繁な修正 Extract Class
OCP 拡張で対応 if/switch の増殖 Strategy, Template Method
LSP 置換可能 instanceof チェック インターフェース再設計
ISP 小さなIF 空のメソッド実装 Interface分割
DIP 抽象に依存 new の直接呼び出し コンストラクタインジェクション

各原則の適用優先度ガイド

優先度 原則 理由
1(最初に) SRP 最も直感的で効果が大きい
2 DIP テスト容易性が劇的に向上
3 OCP 変更の多い箇所で効果を発揮
4 ISP DIPの効果を強化
5 LSP 継承を使う場合に重要

次に読むべきガイド

  • DRY/KISS/YAGNI ── 重複排除と単純化の原則
  • 結合度と凝集度 ── モジュール設計の基盤
  • デメテルの法則 ── 結合度を下げる具体的規則
  • 合成 vs 継承 ── LSPの先にある設計判断
  • デザインパターン: Creational ── OCPとDIPを実現するパターン
  • デザインパターン: Behavioral ── StrategyパターンなどOCP実現手段
  • システム設計: アーキテクチャ ── SOLIDのアーキテクチャレベル適用

参考文献

  1. Robert C. Martin 『Agile Software Development: Principles, Patterns, and Practices』 Prentice Hall, 2002
  2. Robert C. Martin 『Clean Architecture: A Craftsman's Guide to Software Structure and Design』 Prentice Hall, 2017
  3. Barbara Liskov, Jeannette Wing "A Behavioral Notion of Subtyping" ACM Transactions on Programming Languages and Systems, 1994
  4. Bertrand Meyer 『Object-Oriented Software Construction』 Prentice Hall, 1997 (2nd Edition)
  5. Martin Fowler 『Refactoring: Improving the Design of Existing Code』 Addison-Wesley, 2018 (2nd Edition)
  6. Sandi Metz 『Practical Object-Oriented Design: An Agile Primer Using Ruby』 Addison-Wesley, 2018
  7. Michael Feathers 『Working Effectively with Legacy Code』 Prentice Hall, 2004
  8. Mark Seemann 『Dependency Injection: Principles, Practices, and Patterns』 Manning Publications, 2019