ソリッド原則 SOLID

ソリッド原則とは

SOLID は、オブジェクト指向設計の 5 原則の頭文字です。保守性・拡張性・テスタビリティを高め、変更に強い設計を目指します。

  • Single Responsibility Principle(単一責任)
  • Open/Closed Principle(開放/閉鎖)
  • Liskov Substitution Principle(リスコフの置換)
  • Interface Segregation Principle(インターフェース分離)
  • Dependency Inversion Principle(依存関係逆転)

別名: SOLID Principles / 出典に Robert C. Martin(Uncle Bob)らの提唱があります。

概要

ねらい

  • 変更容易性(リグレッションを減らす)
  • 再利用性(差し替え・拡張がしやすい)
  • 可読性(責務が明確)

対象

  • 業務アプリのドメイン/ユースケース層
  • ライブラリ/SDK/プラグイン設計
  • APIクライアント・ストラテジ実装

注意

  • 過剰設計のリスク(YAGNI/KISS とバランス)
  • 小粒化しすぎによる複雑化
  • チームの理解・命名規約との整合

基本

原則一言で要点
SRPクラスは一つの理由でのみ変更される関心の分離・凝集度↑ 結合度↓
OCP拡張に開き、変更に閉じる抽象による拡張ポイント/差し替え
LSP派生は基底を置換可能契約(事前/事後条件・不変条件)
ISPクライアント特化の小さなインターフェース"太い"IFを分割、無駄な実装を排除
DIP詳細ではなく抽象に依存上位ポリシーが下位詳細に依存しない

基礎(ミニチートシート)

良い設計の兆候

  • 依存は抽象(インターフェース)に向く
  • コンストラクタDI / ファクトリで生成責務分離
  • ユニットテスト容易(テストダブル差替OK)

避けたい匂い

  • 巨大クラス(God Object)
  • 条件分岐の乱立(型による分岐→ポリモーフィズム)
  • 下位モジュールへの強い結合(newの直書き乱用)

最小チェックリスト

  • 責務は 1 文で説明できるか(SRP)
  • 機能追加は既存改変よりも拡張で対応できるか(OCP)
  • サブタイプの振る舞いは基底契約を破らないか(LSP)
  • 利用側に不要なメソッドを強要していないか(ISP)
  • 上位ポリシーは実装詳細から独立しているか(DIP)

応用(PHP中心の例)

SRP + OCP + DIP(ストラテジ差替)

<button class="copy" data-copy>Copy</button><?php
interface Notifier { public function send(string $to, string $message): void; }
class MailNotifier implements Notifier { public function send(string $to, string $message): void {/* mail() 等 */} }
class SlackNotifier implements Notifier { public function send(string $to, string $message): void {/* Webhook */} }

class UserRegistrationService { // SRP: 登録のユースケースに責務を限定
  public function __construct(private Notifier $notifier) {}
  public function register(string $email): void {
    // 1) ユーザー作成(省略)
    // 2) 通知(OCP: 実装を増やしても拡張で対応)
    $this->notifier->send($email, 'ようこそ!'); // DIP: 抽象に依存
  }
}

$service = new UserRegistrationService(new MailNotifier());
$service->register('user@example.com');
?>

通知手段追加は Notifier 実装を増やすだけ。既存クラスは変更不要(OCP)。

ISP(太いIFの分割)

<button class="copy" data-copy>Copy</button><?php
// 悪い例: 1つの巨大リポジトリIF
interface Repository { public function find(); public function save(); public function exportCsv(); }

// 良い例: クライアントごとに細分化
interface Reader { public function find(); }
interface Writer { public function save(); }
interface Exportable { public function exportCsv(); }

class UserReader implements Reader {/* ... */}
class UserWriter implements Writer {/* ... */}
?>

発展(テストとアーキテクチャ)

テスト容易性

  • DIによりテストダブル(スタブ/モック)注入
  • ユースケースは副作用を外部に委譲し純粋に
  • 境界(I/O)はアダプタ層に隔離(Ports & Adapters)

レイヤ分離

  • ドメイン/アプリ/インフラ層(Clean/Hexagonal)
  • 上位層は下位詳細に依存しない(DIP)
  • 依存の向きは常に内側へ

アンチパターン回避

  • new の蔓延(Factory/DIに集約)
  • 静的呼び出し濫用(テスト差替困難)
  • 過剰な継承(合成/委譲を優先)

最新情報

  • ニュース準備中です。(/data/news.json を配置すると表示されます)

目的

  • 変更に強いシステムを育てる
  • レビュー/引き継ぎ/オンボーディングの容易化
  • 品質と速度の両立(技術的負債の抑制)

仕事の現場(ベストプラクティス)

DO

  • ユースケースごとに小さいサービスへ分割(SRP)
  • 外部IFはポートで抽象化(DIP)
  • コンストラクタDI + Factoryで生成責務分離
  • 契約テストでLSPを担保

DONT

  • 下位詳細を上位から new 直書き
  • 継承のための継承(共通化は合成で)
  • 何でもかんでも単一メガIF(ISP違反)

そのほかの原則や規約と比較

指針要旨関係性
KISSシンプルに保つSOLIDの過剰化を抑制
DRY重複を避けるSRP/抽象化と相性良い
YAGNI今いらないものは作らないOCPとのバランスが肝
PSR(PHP-FIG)コーディング規約/標準IF実装互換で差替やすい

知っておくといい

知識

  • 抽象・合成・委譲と継承の使い分け
  • 契約による設計(Design by Contract)
  • ポート&アダプタ / クリーンアーキテクチャ

技能

  • 責務分割・命名・境界づけ
  • テストダブル設計(スタブ/モック)
  • 重複/分岐の抽象化(ストラテジ/ステート)

用語・ルール

  • 凝集度/結合度、境界、契約、ポリモーフィズム
  • SRP/OCP/LSP/ISP/DIP の定義とアンチ例
  • PSR-1/12, PSR-4, PSR-7/18 等

LSP(契約を守るサブタイプ)

<button class="copy" data-copy>Copy</button><?php
abstract class Rectangle { protected int $w; protected int $h; public function setWidth(int $w){$this->w=$w;} public function setHeight(int $h){$this->h=$h;} public function area(): int { return $this->w * $this->h; } }
class Square extends Rectangle { // 典型的なLSP違反例
  public function setWidth(int $w){ $this->w=$this->h=$w; }
  public function setHeight(int $h){ $this->w=$this->h=$h; }
}
// → 期待していた矩形の契約(独立に幅/高さを設定できる)が破られる
?>

対策: ビジネス上の意味が異なるなら継承ではなく別型/合成を検討。

DIP(ポート&アダプタ)

<button class="copy" data-copy>Copy</button><?php
interface UserRepository { public function save(array $user): void; }
class PdoUserRepository implements UserRepository { public function __construct(private \PDO $pdo){} public function save(array $user): void { /* INSERT */ } }
class InMemoryUserRepository implements UserRepository { private array $store=[]; public function save(array $user): void { $this->store[]=$user; } }

class RegisterUserUseCase { public function __construct(private UserRepository $repo){} public function __invoke(array $input): void { /* validate... */ $this->repo->save($input); } }

$usecase = new RegisterUserUseCase(new InMemoryUserRepository()); // テスト時に差替
$usecase(['id'=>1,'name'=>'alice']);
?>

より具体的に(リファクタ演習)

Before(匂いのあるコード)

<button class="copy" data-copy>Copy</button><?php
class OrderService {
  public function create(array $order): void {
    // 受注
    // 支払い
    $pdo = new \PDO('dsn','u','p'); // 下位詳細に直接依存(DIP違反)
    $pdo->prepare('INSERT ...');
    // 通知
    mail($order['email'], 'ok', '...'); // 直呼び(差替不能)
  }
}
?>

After(責務分割+抽象依存)

<button class="copy" data-copy>Copy</button><?php
interface PaymentGateway { public function charge(int $yen): void; }
interface OrderRepository { public function save(array $order): void; }
interface Notifier { public function send(string $to, string $msg): void; }

class OrderService {
  public function __construct(
    private PaymentGateway $pay,
    private OrderRepository $repo,
    private Notifier $notify,
  ){}
  public function create(array $order): void {
    $this->pay->charge($order['amount']);
    $this->repo->save($order);
    $this->notify->send($order['email'], '受注完了');
  }
}
?>

生成は Factory / DI コンテナ等に委譲。ユースケースは抽象にのみ依存。