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

5分で理解する SOLID 原則

shogogg
February 21, 2025
58

5分で理解する SOLID 原則

PHPカンファレンス名古屋2025 LT 登壇資料です

shogogg

February 21, 2025
Tweet

Transcript

  1. ⾃⼰紹介 河瀨 翔吾 / Shogo Kawase 株式会社 PR TIMES ソフトウェアエンジニア(2024.12〜)

    シン‧アジャイル コミュニティ運営メンバー 好きな⾔葉 型安全 / アジャイル 好きなアイドル ももいろクローバーZ shogogg shogogg
  2. • Simple Responsibility Principle - 単⼀責任の原則 • Open/Closed Principle -

    開放閉鎖の原則 • Liskov Substitution Principle - リスコフの置換原則 • Interface Segregation Principle - インターフェース分離の原則 • Dependency Inversion Principle - 依存性逆転の原則 SOLID 原則とは
  3. There should never be more than one reason for a

    class to change. 超意訳:クラスを変更する理由は1つだけにしよう。 SRP/単⼀責任の原則
  4. 例えば、こんなクラスがあったとき…… // ユーザー情報取得 API 向けに用意したサービス final readonly class GetUserDataService {

    public function get(int $userId): array { // API の仕様に基づき, ユーザーの情報だけでなく注文履歴も返す . return [ 'user' => ..., 'orders' => ..., ]; } }
  5. Software entities (classes, modules, functions, etc.) should be open for

    extension, but closed for modification. 意訳:クラスや関数は拡張に対して開かれ、修正に対して閉じているべきである。 OCP/開放閉鎖の原則
  6. enum Tax { case TaxFree; // 非課税 case ConsumptionTax; //

    消費税・課税対象 } final readonly class Item { // 商品側で match 式を使って税込価格を計算している public function getPriceWithTax(Tax $tax): int { return match ($tax) { Tax::TaxFree => $this->price, Tax::ConsumptionTax => (int)floor($this->price * 1.1), }; } } 消費税計算の例
  7. enum Tax { case TaxFree; // 非課税 case ConsumptionTax; //

    消費税・課税対象 case ReducedConsumptionTax; // 消費税・課税対象(軽減税率) ⬅ NEW! } final readonly class Item { // $tax に軽減税率が渡されるとエラー 😫 public function getPriceWithTax(Tax $tax): int { return match ($tax) { Tax::TaxFree => $this->price, Tax::ConsumptionTax => (int)floor($this->price * 1.1), }; } } 軽減税率が増えると……?
  8. 軽減税率が増えると……? enum Tax { case TaxFree; // 非課税 case ConsumptionTax;

    // 消費税・課税対象 case ReducedConsumptionTax; // 消費税・課税対象(軽減税率) ⬅ NEW! } final readonly class Item { // match 式を修正する必要がある! public function getPriceWithTax(Tax $tax): int { return match ($tax) { Tax::TaxFree => $this->price, Tax::ConsumptionTax => (int)floor($this->price * 1.1), // ⬇この行を追加する Tax::ReducedConsumptionTax => (int)floor($this->price * 1.08), }; } }
  9. マジメな解消例 // 税率側に計算するメソッドを定義する abstract class Tax { public function compute(int

    $amount): int { return $amount + (int)floor($amount * $this->rate); } } ... // 税率ごとに独立したクラスを定義する ➡ 税率が増えてもクラスを追加するだけで対応できる! final class ConsumptationTax extends Tax { ... } ... // 商品側では税率のメソッドを呼び出すだけ → Tax の種類が増えても変更せずに済む! final readonly class Item { public function getPriceWithTax(Tax $tax): int { return $this->price + $tax->compute($this->price); } }
  10. 妥協的な解消例 // 税率側に計算するメソッドを定義する 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), }; } } // 商品側では税率のメソッドを呼び出すだけ final readonly class Item { public function getPriceWithTax(Tax $tax): int { return $this->price + $tax->compute($this->price); } }
  11. • 機能を追加‧変更する際あちこち修正しないで済むように! ◦ 改修コストを抑える ◦ 修正漏れを防ぐ • 区分値での if ⽂や

    switch ⽂が出てきたら要注意! • OCP に違反すると DRY 原則にも違反しがち。 OCP/開放閉鎖の原則:まとめ
  12. Functions that use pointers or references to base classes must

    be able to use objects of derived classes without knowing it. 意訳:とあるクラスを扱う関数が、その派⽣クラスについて知らなくても扱えるようにしよう。 LSP/リスコフの置換原則
  13. 事前条件が増えている例 class Car { public function start(): void { //

    自動車を発進させる } } class ClassicCar extends Car { private bool $isWarmUpped = false; public function warmUp(): void { $this->isWarmUpped = true; } public function start(): void { // 事前に warmUp メソッドを呼び出していないと例外を投げる if (!$this->isWarmUpped) { throw new Exception('暖機運転が必要です! '); } parent::start(); } }
  14. 事前条件が増えている例 class Driver { public function drive(Car $car): void {

    $car->start(); } } $driver = new Driver(); $driver->drive(new Car()); // 無事に発進できる $driver->drive(new ClassicCar()); // Error: 暖機運転が必要です! 😱
  15. 読み取り専⽤のリポジトリが現れた! interface Repository { public function lookup(int $id): Entity; public

    function store(Entity $entity): void; } // 保存できないのにメソッドの定義は必要. 仕方がないので例外を投げる... final readonly class ReadonlyItemRepository implements Repository { public function lookup(int $id): ReadonlyItem { // ... } public function store(Entity $entity): never { throw new BadMethodCallException('not supported'); } }
  16. こうすれば安⼼だよね? // 読み取り可能なリポジトリのインターフェース interface ReadableRepository { public function lookup(int $id):

    Entity; } // 書き込み可能なリポジトリのインターフェース interface WritableRepository { public function store(Entity $entity): void; } // 読み取り可能なリポジトリのインターフェースだけ実装する! final readonly class ReadonlyItemRepository implements Repository { public function lookup(int $id): ReadonlyItem { // ... } }
  17. High-level modules should not import anything from low-level modules. Both

    should depend on abstractions, not concretions. 意訳:上位モジュールは下位からなにもインポートしてはならない。    さらに上位‧下位を問わず、実装ではなく抽象に依存しよう。 DIP/依存性逆転の原則
  18. // コントローラー(上位=使う側)がサービス(下位=使われる側)に依存している final readonly class ItemController { public function get(int

    $id): Response { $service = new ItemService( /* * なんか沢山の引数 */ ); return $service->get($id); } } 上位が下位に依存している例
  19. // 使う側で欲しいインターフェースを定義する interface ItemService { public function get(int $id): Entity;

    } // コントローラー(使う側)は、細かい事情を知らずにサービスを使える! final readonly class ItemController { private readonly ItemService $itemService; public function get(int $id): Response { return $this->itemService->get($id); } } // サービス(使われる側)は使う側で定義したインターフェースを実装する=使う側に依存する! class ItemServiceImpl implements ItemService { // ... } 依存性を逆転させると……