Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHP開発者のためのSOLID原則再入門 #phpcon / PHP Conference J...

PHP開発者のためのSOLID原則再入門 #phpcon / PHP Conference Japan 2025

2025年6月28日開催の PHP Conference Japan 2025 登壇資料です。

Avatar for shogogg

shogogg

June 26, 2025
Tweet

More Decks by shogogg

Other Decks in Technology

Transcript

  1. Shogo Kawase / @shogogg Jun. 28 2025 PHP Conference Japan

    2025 PHP開発者のための SOLID原則再入門
  2. SOLID原則とは • Simple Responsibility Principle / 単一責任の原則 • Open/Closed Principle

    / 開放閉鎖の原則 • Liskov Substitution Principle / リスコフの置換原則 • Interface Segregation Principle / インターフェース分離の原則 • Dependency Inversion Principle / 依存性逆転の原則
  3. 自己紹介 河瀨 翔吾 / Shogo Kawase 株式会社 PR TIMES ソフトウェアエンジニア(2024.12〜)

    I LOVE... 妻 / 型安全 / アジャイル / ももいろクローバーZ F1 / マリオカート shogogg shogogg
  4. ソフトウェア開発の世界にはいくつも原則がある • DRY原則 / Don’t Repeat Yourself • YAGNI原則 /

    You Aren't Gonna Need It • KISS原則 / Keep It Simple, Stupid • OAOO原則 / Once And Only Once • SOLID原則 • 驚き最小の原則
  5. ソフトウェア開発の世界にはいくつも原則がある • DRY原則 / Don’t Repeat Yourself • YAGNI原則 /

    You Aren't Gonna Need It • KISS原則 / Keep It Simple, Stupid • OAOO原則 / Once And Only Once • SOLID原則 • 驚き最小の原則 すべての原則を理解して常に 守るようにすれば、どんな場 合でも最高のソフトウェアに なるってことだな!
  6. ソフトウェア開発の世界にはいくつも原則がある • DRY原則 / Don’t Repeat Yourself • YAGNI原則 /

    You Aren't Gonna Need It • KISS原則 / Keep It Simple, Stupid • OAOO原則 / Once And Only Once • SOLID原則 • 驚き最小の原則 原則こそ至高! 絶対のルール!
  7. ソフトウェア開発の世界にはいくつも原則がある • DRY原則 / Don’t Repeat Yourself • YAGNI原則 /

    You Aren't Gonna Need It • KISS原則 / Keep It Simple, Stupid • OAOO原則 / Once And Only Once • SOLID原則 • 驚き最小の原則 原則こそ至高! 絶対のルール! 違うよ、全然違うよ
  8. 原則 ≠ 絶対的なルール • 原則同士が対立し、トレードオフの関係になることも DRY/OAOO/SOLID を重視 → 設計が複雑化 →

    KISS原則違反 • 原則は手段であって目的ではない すべての原則は何らかの価値を得るための手段。原則を守ることが目 的ではない。 • 文脈によって価値の優先順位は変わる 「売れるかどうかわからない」場合と「数年間の運用・拡張が決まっ ている」場合では重視する価値が異なる
  9. Don’t Repeat Yourself DRY is a principle of software development

    which states that "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system". By not writing the same information over and over again, our code is more maintainable, more extensible, and less buggy. Getting Started with Rails より
  10. 単一責任の原則 “There should never be more than one reason for

    a class to change.” 意訳: クラスを変更する理由が複数あってはならない。
  11. // 商品を注文する function makeOrder(array $order): void { $isReservation = $this->isReservation($order);

    $this->insertRecord($order); // 予約の場合は決済処理を行わない仕様 if ($isReservation) { $this->makePayment($order); } // 予約かどうかでメールの内容を変える $mailTemplate = $isReservation ? 'reserve' : 'order'; $this->sendmail($mailTemplate, $order); } 予約に対応?任せとけ!!
  12. // 商品を注文する function makeOrder(array $order): void { $isReservation = $this->isReservation($order);

    $isLottery = $this->isLottery($order); // 抽選かどうかで挿入するテーブルが変わる if ($isLottery) { $this->insertLotteryRecord($order); } else { $this->insertOrderRecord($order); } // 予約 or 抽選の場合は決済を行わない仕様 if ($isReservation || $isLottery) { $this->makePayment($order); } // 通常 or 予約 or 抽選でメールの内容を変える $mailTemplate = $this->determineMailTemplate($isReservation, $isLottery); $this->sendmail($mailTemplate, $order); } 抽選機能?余裕だぜ!!
  13. // 商品を注文する function makeOrder(array $order): void { $isReservation = $this->isReservation($order);

    $isLottery = $this->isLottery($order); $isGlitch2 = $this->isGlitch2($order); // 新型ゲーム機「 Glitch 2」の場合は申し込み条件を満たしている必要がある if ($isGlitch2) { $this->ensureGlitch2Requirements($order); } // 抽選かどうかで挿入するテーブルが変わる if ($isLottery) { $this->insertLotteryRecord($order); } else { $this->insertOrderRecord($order); } // 予約 or 抽選の場合は決済を行わない仕様 if ($isReservation || $isLottery) { $this->makePayment($order); } // 通常 or 予約 or 抽選でメールの内容を変える $mailTemplate = $this->determineMailTemplate($isReservation, $isLottery); $this->sendmail($mailTemplate, $order); } 特別な条件の販売にも対応するぞ!
  14. // 商品を注文する function makeOrder(array $order): void { $isReservation = $this->isReservation($order);

    $isLottery = $this->isLottery($order); $isGlitch2 = $this->isGlitch2($order); // 新型ゲーム機「 Glitch 2」の場合は申し込み条件を満たしている必要がある if ($isGlitch2) { $this->ensureGlitch2Requirements($order); } // 抽選かどうかで挿入するテーブルが変わる if ($isLottery) { $this->insertLotteryRecord($order); } else { $this->insertOrderRecord($order); } // 予約 or 抽選の場合は決済を行わない仕様 if ($isReservation || $isLottery) { $this->makePayment($order); } // 通常 or 予約 or 抽選でメールの内容を変える $mailTemplate = $this->determineMailTemplate($isReservation, $isLottery); $this->sendmail($mailTemplate, $order); } テストを書くのがしんどい……
  15. final raedonly class MemberService { // 会員情報を DB から取得して返す function

    getMemberData(int $memberId): array { // 会員情報 + 注文履歴を返す return [ 'member' => $this->lookupMember($memberId), 'orders' => $this->lookupOrders($memberId), ); } } final readonly class MemberController { // 会員情報取得 API function get(int $memberId): Response { // getMemberData を呼び出してレスポンスを返す } } 会員情報取得APIを実装したよ!
  16. final raedonly class MemberService { // 会員情報を DB から取得して返す function

    getMemberData(int $memberId): array { // 会員情報 + 注文履歴を返す return [ 'member' => $this->lookupMember($memberId), 'orders' => $this->lookupOrders($memberId), ); } } final readonly class SomeSubparService { // 会員情報と注文履歴から何かをする処理 function doSomething(int $memberId): array { // getMemberData を呼び出してごにょごにょする } } お、こんなとこに便利なものが! DRY原則だ!流用するぞー!
  17. final raedonly class MemberService { // 会員情報を DB から取得して返す function

    getMemberData(int $memberId): array { // 会員情報 + 注文履歴を返す // 会員情報を返す return [ 'member' => $this->lookupMember($memberId), 'orders' => $this->lookupOrders($memberId), ); } } final readonly class MemberController { // 会員情報取得 API function get(int $memberId): Response { // getMemberData を呼び出してレスポンスを返す } } API仕様が変わったから 注文履歴はレスポンスから消すよ
  18. final raedonly class MemberService { // 会員情報を DB から取得して返す function

    getMemberData(int $memberId): array { // 会員情報 + 注文履歴を返す // 会員情報を返す return [ 'member' => $this->lookupMember($memberId), 'orders' => $this->lookupOrders($memberId), ); } } final readonly class SomeSubparService { // 会員情報と注文履歴から何かをする処理 function doSomething(int $memberId): array { // getMemberData を呼び出してごにょごにょする } } 何もしてないのに壊れた……
  19. OCP/開放閉鎖の原則 “Software entities (classes, modules, functions, etc.) should be open

    for extension, but closed for modification.” 意訳: クラスや関数は拡張に対して開かれ、修正に対して閉じているべきである。
  20. OCP/開放閉鎖の原則 “Software entities (classes, modules, functions, etc.) should be open

    for extension, but closed for modification.” 意訳: クラスや関数は拡張に対して開かれ、修正に対して閉じているべきである。 ちょっと何言ってるか わかんない
  21. // 商品を注文する function makeOrder(array $order): void { $isReservation = $this->isReservation($order);

    $isLottery = $this->isLottery($order); $isGlitch2 = $this->isGlitch2($order); // 新型ゲーム機「 Glitch 2」の場合は申し込み条件を満たしている必要がある if ($isGlitch2) { $this->ensureGlitch2Requirements($order); } // 抽選かどうかで挿入するテーブルが変わる if ($isLottery) { $this->insertLotteryRecord($order); } else { $this->insertOrderRecord($order); } // 予約 or 抽選の場合は決済を行わない仕様 if ($isReservation || $isLottery) { $this->makePayment($order); } // 通常 or 予約 or 抽選でメールの内容を変える $mailTemplate = $this->determineMailTemplate($isReservation, $isLottery); $this->sendmail($mailTemplate, $order); }
  22. interface OrderStrategyInterface { public function makeOrder(array $order): void; } final

    class OrdinaryOrderStrategy implements OrderStrategyInterface { // 通常注文向けの実装 ... } final class ReservationOrderStrategy implements OrderStrategyInterface { // 予約注文向けの実装 ... } // ... final class OrderStrategyFactory { public function create(array $order): OrderStrategyInterface { // 注文内容に応じて適切な OrderStrategyInterface の実装を返す... } } // 商品を注文する function makeOrder(OrderStrategyFactory $factory, array $order): void { $strategy = $factory->create($order); $strategy->makeOrder($order); }
  23. interface OrderStrategyInterface { public function makeOrder(array $order): void; } final

    class OrdinaryOrderStrategy implements OrderStrategyInterface { // 通常注文向けの実装 ... } final class ReservationOrderStrategy implements OrderStrategyInterface { // 予約注文向けの実装 ... } // ... final class OrderStrategyFactory { public function create(array $order): OrderStrategyInterface { // 注文内容に応じて適切な OrderStrategyInterface の実装を返す... } } // 商品を注文する function makeOrder(OrderStrategyFactory $factory, array $order): void { $strategy = $factory->create($order); $strategy->makeOrder($order); } 注文種別ごとの戦略を定義するインターフェース
  24. interface OrderStrategyInterface { public function makeOrder(array $order): void; } final

    class OrdinaryOrderStrategy implements OrderStrategyInterface { // 通常注文向けの実装 ... } final class ReservationOrderStrategy implements OrderStrategyInterface { // 予約注文向けの実装 ... } // ... final class OrderStrategyFactory { public function create(array $order): OrderStrategyInterface { // 注文内容に応じて適切な OrderStrategyInterface の実装を返す... } } // 商品を注文する function makeOrder(OrderStrategyFactory $factory, array $order): void { $strategy = $factory->create($order); $strategy->makeOrder($order); } 注文種別ごとに異なる実装(クラス)を定義する
  25. interface OrderStrategyInterface { public function makeOrder(array $order): void; } final

    class OrdinaryOrderStrategy implements OrderStrategyInterface { // 通常注文向けの実装 ... } final class ReservationOrderStrategy implements OrderStrategyInterface { // 予約注文向けの実装 ... } // ... final class OrderStrategyFactory { public function create(array $order): OrderStrategyInterface { // 注文内容に応じて適切な OrderStrategyInterface の実装を返す ... } } // 商品を注文する function makeOrder(OrderStrategyFactory $factory, array $order): void { $strategy = $factory->create($order); $strategy->makeOrder($order); } 戦略を選択するためのファクトリークラスを実装
  26. interface OrderStrategyInterface { public function makeOrder(array $order): void; } final

    class OrdinaryOrderStrategy implements OrderStrategyInterface { // 通常注文向けの実装 ... } final class ReservationOrderStrategy implements OrderStrategyInterface { // 予約注文向けの実装 ... } // ... final class OrderStrategyFactory { public function create(array $order): OrderStrategyInterface { // 注文内容に応じて適切な OrderStrategyInterface の実装を返す... } } // 商品を注文する function makeOrder(OrderStrategyFactory $factory, array $order): void { $strategy = $factory->create($order); $strategy->makeOrder($order); } 注文処理からは適切な戦略を呼び出す
  27. // 消費税を表す列挙型 enum Tax { case TaxFree; // 非課税 case

    ConsumptionTax; // 消費税・課税対象 } // 商品クラス final readonly class Item { public function getPriceWithTax(Tax $tax): int { return match ($tax) { Tax::TaxFree => $this->price, Tax::ConsumptionTax => (int)floor($this->price * 1.10), }; } }
  28. // 消費税を表す列挙型 enum Tax { case TaxFree; // 非課税 case

    ConsumptionTax; // 消費税・課税対象 } // 商品クラス final readonly class Item { public function getPriceWithTax(Tax $tax): int { return match ($tax) { Tax::TaxFree => $this->price, Tax::ConsumptionTax => (int)floor($this->price * 1.10), }; } } 商品クラス側で match 式を使って計算している
  29. // 消費税を表す列挙型 enum Tax { case TaxFree; // 非課税 case

    ConsumptionTax; // 消費税・課税対象 case ReducedConsumptionTax; // 消費税・課税対象(軽減税率) } // 商品クラス final readonly class Item { public function getPriceWithTax(Tax $tax): int { return match ($tax) { Tax::TaxFree => $this->price, Tax::ConsumptionTax => (int)floor($this->price * 1.10), }; } } 消費税率に「軽減税率」が増えた!
  30. // 消費税を表す列挙型 enum Tax { case TaxFree; // 非課税 case

    ConsumptionTax; // 消費税・課税対象 case ReducedConsumptionTax; // 消費税・課税対象(軽減税率) } // 商品クラス final readonly class Item { public function getPriceWithTax(Tax $tax): int { return match ($tax) { Tax::TaxFree => $this->price, Tax::ConsumptionTax => (int)floor($this->price * 1.10), }; } } $tax に軽減税率が渡されるとエラーが起きる!
  31. // 消費税を表す列挙型 enum Tax { case TaxFree; // 非課税 case

    ConsumptionTax; // 消費税・課税対象 case ReducedConsumptionTax; // 消費税・課税対象(軽減税率) } // 商品クラス final readonly class Item { public function getPriceWithTax(Tax $tax): int { return match ($tax) { Tax::TaxFree => $this->price, Tax::ConsumptionTax => (int)floor($this->price * 1.10), Tax::ReducedConsumptionTax => (int)floor($this->price * 1.08), }; } } 税率が増えるたびに match 式の条件を増やす必要がある
  32. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 消費税を表す型 abstract class Tax { public function compute(int $amount): int { return (int)floor($amount * $this->rate / 100); } } // 非課税 final class TaxFree extends Tax { protected int $rate = 0; } // 消費税・課税対象 final class ConsumptionTax extends Tax { protected int $rate = 10; }
  33. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 消費税を表す型 abstract class Tax { public function compute(int $amount): int { return (int)floor($amount * $this->rate / 100); } } // 非課税 final class TaxFree extends Tax { protected int $rate = 0; } // 消費税・課税対象 final class ConsumptionTax extends Tax { protected int $rate = 10; } 消費税を表す型をインターフェースや抽象クラスとして定義
  34. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 消費税を表す型 abstract class Tax { public function compute(int $amount): int { return (int)floor($amount * $this->rate / 100); } } // 非課税 final class TaxFree extends Tax { protected int $rate = 0; } // 消費税・課税対象 final class ConsumptionTax extends Tax { protected int $rate = 10; } 税額の計算は消費税クラスに実装する
  35. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 消費税を表す型 abstract class Tax { public function compute(int $amount): int { return (int)floor($amount * $this->rate / 100); } } // 非課税 final class TaxFree extends Tax { protected int $rate = 0; } // 消費税・課税対象 final class ConsumptionTax extends Tax { protected int $rate = 10; } 税率ごとにサブクラスを定義する
  36. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 消費税を表す型 abstract class Tax { public function compute(int $amount): int { return (int)floor($amount * $this->rate / 100); } } // 非課税 final class TaxFree extends Tax { protected int $rate = 0; } // 消費税・課税対象 final class ConsumptionTax extends Tax { protected int $rate = 10; } 商品クラスは渡された税率のメソッドを呼び出すだけ
  37. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 消費税を表す型 abstract class Tax { public function compute(int $amount): int { return (int)floor($amount * $this->rate / 100); } } // 非課税 final class TaxFree extends Tax { protected int $rate = 0; } // 消費税・課税対象 final class ConsumptionTax extends Tax { protected int $rate = 10; } // 消費税・課税対象(軽減税率) final class ReducedConsumptionTax extends Tax { protected int $rate = 8; } 新しい税率は新しいクラスを定義
  38. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 消費税を表す型 abstract class Tax { public function compute(int $amount): int { return (int)floor($amount * $this->rate / 100); } } // 非課税 final class TaxFree extends Tax { protected int $rate = 0; } // 消費税・課税対象 final class ConsumptionTax extends Tax { protected int $rate = 10; } // 消費税・課税対象(軽減税率) final class ReducedConsumptionTax extends Tax { protected int $rate = 8; } 新しい税率は新しいクラスを定義 既存の商品クラスは変更せずに機能追加ができた!
  39. • シンプルな処理だから、と安直に if 文や switch 文、match 式を書く のではなく、知識・情報を1ヶ所にまとめるよう抽象化する ことで、 自然と

    OCP に沿った設計となる • でも PHP だと、この規模のコードをここまで徹底してクラスに分ける ことで逆に可読性や保守性を下げる場合もある のでは……? OCP適用例:DRY原則を守る
  40. // Java の例(enum がパラメータを持てる) public enum Tax { TaxFree(0), ConsumptionTax(10),

    private final int rate; private Tax(final int rate) { this.rate = rate; } public int amount(final int price) { return price * rate / 100; } }
  41. // Java の例(enum でオーバーライド) public enum Tax { // 非課税

    TaxFree { @Override public int amount(final int price) { return 0; } }, // 消費税・課税対象 ConsumptionTax { @Override public int amount(final int price) { return price * rate / 100; } }, public abstract int amount(final int price); }
  42. // Scala の例 (sealed trait) sealed trait Tax { def

    amount(price: Int): Int } // 非課税 case class TaxFree() extends Tax { override def amount(price: Int): Int = 0 } // 消費税・課税対象 case class ConsumptionTax() extends Tax { override def amount(price: Int): Int = price + (price * 0.1).toInt }
  43. // Kotlin の例(sealed interface) sealed interface Tax { fun amount(price:

    Int): Int } // 非課税 data class TaxFree : Tax { override fun amount(price: Int): Int = 0 } // 消費税・課税対象 data class ConsumptionTax : Tax { override fun amount(price: Int): Int = (price * 0.1).toInt() }
  44. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 税率側に計算するメソッドを定義する enum Tax { ... public function compute(int $amount): int { return match ($this) { self::TaxFree => $amount, self::ConsumptionTax => $amount + (int)($amount * 1.1), self::ReducedConsumptionTax => $amount + (int)floor($amount * 1.08), }; } } 商品クラスは渡された税率のメソッドを呼び出すだけ
  45. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 税率側に計算するメソッドを定義する enum Tax { ... public function compute(int $amount): int { return match ($this) { self::TaxFree => $amount, self::ConsumptionTax => $amount + (int)($amount * 1.1), self::ReducedConsumptionTax => $amount + (int)floor($amount * 1.08), }; } } 税率側に列挙子ごとの処理を match 式等で記述
  46. // 商品クラス final readonly class Item { public function getPriceWithTax(Tax

    $tax): int { return $this->price + $tax->compute($this->price); } } // 税率側に計算するメソッドを定義する enum Tax { ... public function compute(int $amount): int { return match ($this) { self::TaxFree => $amount, self::ConsumptionTax => $amount + (int)($amount * 1.1), self::ReducedConsumptionTax => $amount + (int)floor($amount * 1.08), }; } } 常に列挙子を網羅するテストを書くこと!
  47. 開放閉鎖の原則:まとめ 高い拡張性と保守性を両立するため コードの「修正」ではなく「追 加」によって機能が拡張できる ように設計しよう • ストラテジーパターン やDRY原則に沿った設計 がポイント •

    将来的な拡張が予想される部分で適用 すると威力を発揮する 例)消費税率、配送方法、支払方法、etc... • 「区分」「種類」等での if/switch/match が出てきたら OCP を 適用できないか検討してみよう • PHP でやるときは可読性・保守性とのバランスも意識したい
  48. LSP/リスコフの置換原則 “Functions that use pointers or references to base classes

    must be able to use objects of derived classes without knowing it.” 意訳: とあるクラスを扱う関数は、その派生クラスについて知らずに扱うことができ なくてはならない。
  49. Photo: Kenneth C. Zirkel, CC BY-SA 3.0, via Wikimedia Commons

    Barbara Liskov (1939.11 〜) • 米国で女性初の博士(Computer Science) • MIT教授(Computer Science) • 2008年にチューリング賞を受賞
  50. 例:こんなホテル予約サイトはイヤだ • ホテルAの場合:予約前に電話で確認が必要 ➡ 事前条件が増えている! • ホテルBの場合:予約後に電報を送らないと予約が完了しない ➡ 事後条件が減っている! •

    ホテルCの場合:当日行ってみたらホテルじゃなくてスパだった ➡ 不変条件が変わっている! • ホテルDの場合:施設側の選り好みで予約不可の場合がある ➡ 例外の種類が増えている!
  51. 例:こんなホテル予約サイトはイヤだ • ホテルAの場合:予約前に電話で確認が必要 ➡ 事前条件が増えている! • ホテルBの場合:予約後に電報を送らないと予約が完了しない ➡ 事後条件が減っている! •

    ホテルCの場合:当日行ってみたらホテルじゃなくてスパだった ➡ 不変条件が変わっている! • ホテルDの場合:施設側の選り好みで予約不可の場合がある ➡ 例外の種類が増えている! プログラミングも一緒だよね
  52. class FileStorage { public function store(string $src, string $dir, array

    $options = []): void { // $src を $dir ディレクトリに保存する処理 // $options には保存に関するオプションが含まれる // $options が空でも問題なく動作する // ... } } class S3FileStorage extends FileStorage { public function store(string $src, string $dir, array $options = []): void { if (!isset($options['bucket']) || $options['bucket'] === '') { throw new InvalidArgumentException('Bucket name is required!!'); } // ... } } // FireStorage を使ってファイルを保存する関数 function storeFile(FileStorage $storage, string $src, string $dir): void { $storage->store($src, $dir); } storeFile(new FileStorage(), 'source.txt', 'awesome/dir/path'); // OK storeFile(new S3FileStorage(), 'source.txt', 'awesome/dir/path'); // ERROR!
  53. class FileStorage { public function store(string $src, string $dir, array

    $options = []): void { // $src を $dir ディレクトリに保存する処理 // $options には保存に関するオプションが含まれる // $options が空でも問題なく動作する // ... } } class S3FileStorage extends FileStorage { public function store(string $src, string $dir, array $options = []): void { if (!isset($options['bucket']) || $options['bucket'] === '') { throw new InvalidArgumentException('Bucket name is required!!'); } // ... } } // FireStorage を使ってファイルを保存する関数 function storeFile(FileStorage $storage, string $src, string $dir): void { $storage->store($src, $dir); } storeFile(new FileStorage(), 'source.txt', 'awesome/dir/path'); // OK storeFile(new S3FileStorage(), 'source.txt', 'awesome/dir/path'); // ERROR! S3 の場合は $options に bucket がないと例外を投げている
  54. class FileStorage { public function store(string $src, string $dir, array

    $options = []): void { // $src を $dir ディレクトリに保存する処理 // $options には保存に関するオプションが含まれる // $options が空でも問題なく動作する // ... } } class S3FileStorage extends FileStorage { public function store(string $src, string $dir, array $options = []): void { if (!isset($options['bucket']) || $options['bucket'] === '') { throw new InvalidArgumentException('Bucket name is required!!'); } // ... } } // FireStorage を使ってファイルを保存する関数 function storeFile(FileStorage $storage, string $src, string $dir): void { $storage->store($src, $dir); } storeFile(new FileStorage(), 'source.txt', 'awesome/dir/path'); // OK storeFile(new S3FileStorage(), 'source.txt', 'awesome/dir/path'); // ERROR! storeFile 関数は FileStorage の実装が何かを知らずに使いたい
  55. class FileStorage { public function store(string $src, string $dir, array

    $options = []): void { // $src を $dir ディレクトリに保存する処理 // $options には保存に関するオプションが含まれる // $options が空でも問題なく動作する // ... } } class S3FileStorage extends FileStorage { public function store(string $src, string $dir, array $options = []): void { if (!isset($options['bucket']) || $options['bucket'] === '') { throw new InvalidArgumentException('Bucket name is required!!'); } // ... } } // FireStorage を使ってファイルを保存する関数 function storeFile(FileStorage $storage, string $src, string $dir): void { $storage->store($src, $dir); } storeFile(new FileStorage(), 'source.txt', 'awesome/dir/path'); // OK storeFile(new S3FileStorage(), 'source.txt', 'awesome/dir/path'); // ERROR! FileStorage は $options を省略しても動く
  56. class FileStorage { public function store(string $src, string $dir, array

    $options = []): void { // $src を $dir ディレクトリに保存する処理 // $options には保存に関するオプションが含まれる // $options が空でも問題なく動作する // ... } } class S3FileStorage extends FileStorage { public function store(string $src, string $dir, array $options = []): void { if (!isset($options['bucket']) || $options['bucket'] === '') { throw new InvalidArgumentException('Bucket name is required!!'); } // ... } } // FireStorage を使ってファイルを保存する関数 function storeFile(FileStorage $storage, string $src, string $dir): void { $storage->store($src, $dir); } storeFile(new FileStorage(), 'source.txt', 'awesome/dir/path'); // OK storeFile(new S3FileStorage(), 'source.txt', 'awesome/dir/path'); // ERROR! S3FileStorage に差し替えると例外発生!
  57. class FileStorage { public function getFileInfo(string $path): array { return

    [ 'size' => ..., 'created_at' => ..., 'permissions' => ..., ]; } } class S3FileStorage extends FileStorage { public function getFileInfo(string $path): array { // S3 にはファイルパーミッションがないので省略する return [ 'size' => ..., 'created_at' => ..., ]; } } function doSomething(FileStorage $storage, string $path): void { $info = $storage->getFileInfo($path); // permissions を使って何か処理をする } doSomething(new FileStorage(), '/path/to/file'); // OK doSomething(new S3FileStorage(), '/path/to/s3/file'); // Undefined array key "permissions" in ...
  58. class FileStorage { public function getFileInfo(string $path): array { return

    [ 'size' => ..., 'created_at' => ..., 'permissions' => ..., ]; } } class S3FileStorage extends FileStorage { public function getFileInfo(string $path): array { // S3 にはファイルパーミッションがないので省略する return [ 'size' => ..., 'created_at' => ..., ]; } } function doSomething(FileStorage $storage, string $path): void { $info = $storage->getFileInfo($path); // permissions を使って何か処理をする } doSomething(new FileStorage(), '/path/to/file'); // OK doSomething(new S3FileStorage(), '/path/to/s3/file'); // Undefined array key "permissions" in ... ファイルサイズ・作成日時・パーミッションを返す
  59. class FileStorage { public function getFileInfo(string $path): array { return

    [ 'size' => ..., 'created_at' => ..., 'permissions' => ..., ]; } } class S3FileStorage extends FileStorage { public function getFileInfo(string $path): array { // S3 にはファイルパーミッションがないので省略する return [ 'size' => ..., 'created_at' => ..., ]; } } function doSomething(FileStorage $storage, string $path): void { $info = $storage->getFileInfo($path); // permissions を使って何か処理をする } doSomething(new FileStorage(), '/path/to/file'); // OK doSomething(new S3FileStorage(), '/path/to/s3/file'); // Undefined array key "permissions" in ... S3 にはパーミッションがないから、と戻り値の要素を減らしている
  60. class FileStorage { public function getFileInfo(string $path): array { return

    [ 'size' => ..., 'created_at' => ..., 'permissions' => ..., ]; } } class S3FileStorage extends FileStorage { public function getFileInfo(string $path): array { // S3 にはファイルパーミッションがないので省略する return [ 'size' => ..., 'created_at' => ..., ]; } } function doSomething(FileStorage $storage, string $path): void { $info = $storage->getFileInfo($path); // permissions を使って何か処理をする } doSomething(new FileStorage(), '/path/to/file'); // OK doSomething(new S3FileStorage(), '/path/to/s3/file'); // Undefined array key "permissions" in ... doSomething 関数は FileStorage の実装が何かを知らずに使いたい
  61. class FileStorage { public function getFileInfo(string $path): array { return

    [ 'size' => ..., 'created_at' => ..., 'permissions' => ..., ]; } } class S3FileStorage extends FileStorage { public function getFileInfo(string $path): array { // S3 にはファイルパーミッションがないので省略する return [ 'size' => ..., 'created_at' => ..., ]; } } function doSomething(FileStorage $storage, string $path): void { $info = $storage->getFileInfo($path); // permissions を使って何か処理をする } doSomething(new FileStorage(), '/path/to/file'); // OK doSomething(new S3FileStorage(), '/path/to/s3/file'); // Undefined array key "permissions" in ... もちろん FileStorage を使えばちゃんと動く
  62. class FileStorage { public function getFileInfo(string $path): array { return

    [ 'size' => ..., 'created_at' => ..., 'permissions' => ..., ]; } } class S3FileStorage extends FileStorage { public function getFileInfo(string $path): array { // S3 にはファイルパーミッションがないので省略する return [ 'size' => ..., 'created_at' => ..., ]; } } function doSomething(FileStorage $storage, string $path): void { $info = $storage->getFileInfo($path); // permissions を使って何か処理をする } doSomething(new FileStorage(), '/path/to/file'); // OK doSomething(new S3FileStorage(), '/path/to/s3/file'); // Undefined array key "permissions" in ... S3FileStorage に差し替えるとエラーが発生!
  63. ISP/インタフェース分離の原則 “Many client-specific interfaces are better than one general-purpose interface.”

    意訳: 汎用インターフェースが1つだけあるよりも、 要求ごとに分離された複数のインターフェースがあった方がよい。
  64. // リポジトリインターフェース interface Repository { public function lookup(int $id): Entity;

    public function store(Entity $entity): void; public function remove(int $id): void; } 参照・保存・削除ができるリポジトリのインターフェース
  65. // リポジトリインターフェース interface Repository { public function lookup(int $id): Entity;

    public function store(Entity $entity): void; public function remove(int $id): void; } // 読み取り専用リポジトリ final readonly class ReadonlyItemRepository implements Repository { public function lookup(int $id): ReadonlyItem { // ... } public function store(Entity $entity): never { throw new BadMethodCallException('not supported'); } public function remove(int $id): never { throw new BadMethodCallException('not supported'); } } 読み取り専用のリポジトリが必要になった
  66. // リポジトリインターフェース interface Repository { public function lookup(int $id): Entity;

    public function store(Entity $entity): void; public function remove(int $id): void; } // 読み取り専用リポジトリ final readonly class ReadonlyItemRepository implements Repository { public function lookup(int $id): ReadonlyItem { // ... } public function store(Entity $entity): never { throw new BadMethodCallException('not supported'); } public function remove(int $id): never { throw new BadMethodCallException('not supported'); } } メソッドの定義が必要なので仕方なく例外を投げる……
  67. // 読み取り可能なリポジトリのインターフェース interface ReadableRepository { public function lookup(int $id): Entity;

    } // 書き込み可能なリポジトリのインターフェース interface WritableRepository { public function store(Entity $entity): void; public function remove(int $id): void; } // 一般的なリポジトリ final readonly class ItemRepository implements ReadableRepository, WritableRepository { public function lookup(int $id): Entity { // ... } public function store(Entity $entity): void { // ... } public function remove(int $id): void { // ... } } // 読み取り専用リポジトリ final readonly class ReadOnlyItemRepository implements ReadableRepository { public function lookup(int $id): Entity { // ... } }
  68. // 読み取り可能なリポジトリのインターフェース interface ReadableRepository { public function lookup(int $id): Entity;

    } // 書き込み可能なリポジトリのインターフェース interface WritableRepository { public function store(Entity $entity): void; public function remove(int $id): void; } // 一般的なリポジトリ final readonly class ItemRepository implements ReadableRepository, WritableRepository { public function lookup(int $id): Entity { // ... } public function store(Entity $entity): void { // ... } public function remove(int $id): void { // ... } } // 読み取り専用リポジトリ final readonly class ReadOnlyItemRepository implements ReadableRepository { public function lookup(int $id): Entity { // ... } } 用途別に細かくインターフェースを定義する
  69. // 読み取り可能なリポジトリのインターフェース interface ReadableRepository { public function lookup(int $id): Entity;

    } // 書き込み可能なリポジトリのインターフェース interface WritableRepository { public function store(Entity $entity): void; public function remove(int $id): void; } // 一般的なリポジトリ final readonly class ItemRepository implements ReadableRepository, WritableRepository { public function lookup(int $id): Entity { // ... } public function store(Entity $entity): void { // ... } public function remove(int $id): void { // ... } } // 読み取り専用リポジトリ final readonly class ReadOnlyItemRepository implements ReadableRepository { public function lookup(int $id): Entity { // ... } } 必要なインターフェースを複数実装する
  70. // 読み取り可能なリポジトリのインターフェース interface ReadableRepository { public function lookup(int $id): Entity;

    } // 書き込み可能なリポジトリのインターフェース interface WritableRepository { public function store(Entity $entity): void; public function remove(int $id): void; } // 一般的なリポジトリ final readonly class ItemRepository implements ReadableRepository, WritableRepository { public function lookup(int $id): Entity { // ... } public function store(Entity $entity): void { // ... } public function remove(int $id): void { // ... } } // 読み取り専用リポジトリ final readonly class ReadOnlyItemRepository implements ReadableRepository { public function lookup(int $id): Entity { // ... } } 余分なメソッドの定義がなくなってスッキリ!
  71. DIP/依存性逆転の原則 “High-level modules should not import anything from low-level modules.

    Both should depend on abstractions, not concretions.” 意訳: 上位モジュールは下位からなにもインポートしてはならない。 さらに上位・下位を問わず、実装ではなく抽象に依存すべきである。
  72. • 依存性の注入(DI) コードから直接依存して new するのではなく、外部から依存を注入 す ることで疎結合となり、関心が分離できる • 腐敗防止層の導入 フレームワークやライブラリを直接呼び出すのではなく、使う側の関心

    に合わせて腐敗防止層 を導入し、技術的詳細の変化に対応しやすくする • Clean Architecture DIP の権化とも言えるアーキテクチャなので、クリーンアーキテクチャ に沿うように設計すれば自然と DIP にも沿った設計になる(はず) 依存性逆転の法則:どのように実現するの?
  73. final readonly class FileUploadService { public function upload(UploadedFile $file): void

    { $client = new S3Client([ // S3 接続先などの設定 ... ]); $client->putObject([ // アップロードするファイルの設定 ... ]); } } ユーザーがアップロードしたファイルを保存する処理
  74. final readonly class FileUploadService { public function upload(UploadedFile $file): void

    { $client = new S3Client([ // S3 接続先などの設定 ... ]); $client->putObject([ // アップロードするファイルの設定 ... ]); } } 技術的な詳細(S3)に直接依存してしまっている
  75. final readonly class FileUploadService { public function __construct( private S3ClientInterface

    $s3Client ) {} public function upload(UploadedFile $file): void { $this->s3Client->putObject([ // アップロードするファイルの設定 ... ]); } } S3Client を DI で注入すればOK?
  76. final readonly class FileUploadService { public function __construct( private S3ClientInterface

    $s3Client ) {} public function upload(UploadedFile $file): void { $this->s3Client->putObject([ // アップロードするファイルの設定 ... ]); } } FileUploadService はアップロードされたファイルを 永続化したいだけ、保存先には興味がない
  77. interface FileStorageInterface { public function upload(UploadedFile $file): void; } final

    readonly class S3FileStorage implements FileStorageInterface { public function __construct( private S3ClientInterface $s3Client ) {} public function upload(UploadedFile $file): void { // S3 へファイルをアップロードする処理 ... } } final readonly class FileUploadService { public function __construct( private FileStorageInterface $storage ) {} public function upload(UploadedFile $file): void { $this->storage->upload($file); // その他アップロードに関する処理 ... } }
  78. interface FileStorageInterface { public function upload(UploadedFile $file): void; } final

    readonly class S3FileStorage implements FileStorageInterface { public function __construct( private S3ClientInterface $s3Client ) {} public function upload(UploadedFile $file): void { // S3 へファイルをアップロードする処理 ... } } final readonly class FileUploadService { public function __construct( private FileStorageInterface $storage ) {} public function upload(UploadedFile $file): void { $this->storage->upload($file); // その他アップロードに関する処理 ... } } ファイルを保存したい、というビジネス側の都合で インターフェースを定義する
  79. interface FileStorageInterface { public function upload(UploadedFile $file): void; } final

    readonly class S3FileStorage implements FileStorageInterface { public function __construct( private S3ClientInterface $s3Client ) {} public function upload(UploadedFile $file): void { // S3 へファイルをアップロードする処理 ... } } final readonly class FileUploadService { public function __construct( private FileStorageInterface $storage ) {} public function upload(UploadedFile $file): void { $this->storage->upload($file); // その他アップロードに関する処理 ... } } ビジネス側の都合に合わせて技術的な詳細を実装する
  80. interface FileStorageInterface { public function upload(UploadedFile $file): void; } final

    readonly class S3FileStorage implements FileStorageInterface { public function __construct( private S3ClientInterface $s3Client ) {} public function upload(UploadedFile $file): void { // S3 へファイルをアップロードする処理 ... } } final readonly class FileUploadService { public function __construct( private FileStorageInterface $storage ) {} public function upload(UploadedFile $file): void { $this->storage->upload($file); // その他アップロードに関する処理 ... } } ビジネスロジックから技術的な詳細への依存が消えた