Slide 1

Slide 1 text

5分で理解する SOLID 原則 Feb. 22 2025 PHP Conference 名古屋 2025 Shogo Kawase / @shogogg

Slide 2

Slide 2 text

⾃⼰紹介 河瀨 翔吾 / Shogo Kawase 株式会社 PR TIMES ソフトウェアエンジニア(2024.12〜) シン‧アジャイル コミュニティ運営メンバー 好きな⾔葉 型安全 / アジャイル 好きなアイドル ももいろクローバーZ shogogg shogogg

Slide 3

Slide 3 text

● Simple Responsibility Principle - 単⼀責任の原則 ● Open/Closed Principle - 開放閉鎖の原則 ● Liskov Substitution Principle - リスコフの置換原則 ● Interface Segregation Principle - インターフェース分離の原則 ● Dependency Inversion Principle - 依存性逆転の原則 SOLID 原則とは

Slide 4

Slide 4 text

なんだかむずかしい……😫

Slide 5

Slide 5 text

なんのために守るのかよくわからん🤔

Slide 6

Slide 6 text

そんなあなたのためのLTです!󰚖

Slide 7

Slide 7 text

1原則/分で紹介すればイケる!!󰗧

Slide 8

Slide 8 text

Simple Responsibility Prinsiple 単⼀責任の原則

Slide 9

Slide 9 text

There should never be more than one reason for a class to change. 超意訳:クラスを変更する理由は1つだけにしよう。 SRP/単⼀責任の原則

Slide 10

Slide 10 text

クラスや関数に⾊んな処理を 詰め込むなってことだな! 完全に理解した!! つまり?

Slide 11

Slide 11 text

クラスや関数に⾊んな処理を 詰め込むな、ってことだな!? 単⼀責任の原則、完全に理解した! つまり? 違うよ、全然違うよ

Slide 12

Slide 12 text

例えば、こんなクラスがあったとき…… // ユーザー情報取得 API 向けに用意したサービス final readonly class GetUserDataService { public function get(int $userId): array { // API の仕様に基づき, ユーザーの情報だけでなく注文履歴も返す . return [ 'user' => ..., 'orders' => ..., ]; } }

Slide 13

Slide 13 text

ユーザー情報と注⽂履歴が欲しいん だよなぁ…… おっ、ちょうど便利なクラスが! 後⽇……

Slide 14

Slide 14 text

ユーザー情報と注⽂履歴が欲しいん だよなぁ…… おっ、ちょうど便利なクラスが! 後⽇…… ちょっと待った!

Slide 15

Slide 15 text

後⽇、前述のサービスを修正するときに…… ● 予期せぬ箇所で問題が起こるかも? ➡ 品質の低下 ● 影響範囲が増え、検証範囲が増える ➡ ⼯数の増加 ● 怖くて既存のコードが修正しづらい ➡ 技術的負債の増加 これを許すとどうなる?

Slide 16

Slide 16 text

● ひとつのクラスや関数に機能を詰め込みすぎない! ● だけじゃなく、そこにあるクラスを軽率に使い回さない! ● 勘違い DRY 原則主義者に要注意。 SRP/単⼀責任の原則:まとめ

Slide 17

Slide 17 text

https://fortee.jp/phpcon-nagoya-2025/proposal/a43b3ab1-39b2-43d0-afdb-8e6428de0b96

Slide 18

Slide 18 text

Open/Closed Prinsiple 開放閉鎖の原則

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

ソフトウェアに機能を追加する際に ● 修正に対して閉じている ➡ 既存のクラス等を修正せずに ● 拡張に対して開いている ➡ 新しいクラス等の追加で対応できる ようにしましょう、ということ。 つまり、どういうことだってばよ?

Slide 21

Slide 21 text

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), }; } } 消費税計算の例

Slide 22

Slide 22 text

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), }; } } 軽減税率が増えると……?

Slide 23

Slide 23 text

軽減税率が増えると……? 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), }; } }

Slide 24

Slide 24 text

マジメな解消例 // 税率側に計算するメソッドを定義する 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); } }

Slide 25

Slide 25 text

妥協的な解消例 // 税率側に計算するメソッドを定義する 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); } }

Slide 26

Slide 26 text

● 機能を追加‧変更する際あちこち修正しないで済むように! ○ 改修コストを抑える ○ 修正漏れを防ぐ ● 区分値での if ⽂や switch ⽂が出てきたら要注意! ● OCP に違反すると DRY 原則にも違反しがち。 OCP/開放閉鎖の原則:まとめ

Slide 27

Slide 27 text

Liskov Substitution Prinsiple LSP: リスコフの置換原則

Slide 28

Slide 28 text

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. 意訳:とあるクラスを扱う関数が、その派⽣クラスについて知らなくても扱えるようにしよう。 LSP/リスコフの置換原則

Slide 29

Slide 29 text

リスコフ is 誰? バーバラ‧リスコフ Photo: Kenneth C. Zirkel, CC BY-SA 3.0, via Wikimedia Commons

Slide 30

Slide 30 text

● ホテルAの場合:予約前に電話で確認が必要。 ● ホテルBの場合:予約後に電報を送らないと完了しない。 ● ホテルCの場合:実はスパ施設である。 ● ホテルDの場合:電話番号が080〜だとエラーで予約できない。 こんなホテル予約サイトは嫌だ

Slide 31

Slide 31 text

● ホテルAの場合:予約前に電話で確認が必要。 事前条件が増えている!😫 ● ホテルBの場合:予約後に電報を送らないと完了しない。 事後条件が減っている!😫 ● ホテルCの場合:実はスパ施設である。 不変条件が変わっている!😫 ● ホテルDの場合:電話番号が080〜だとエラーで予約できない。 例外の種類が増えている!😫 こんなホテル予約サイトは嫌だ

Slide 32

Slide 32 text

● ホテルAの場合:予約前に電話で確認が必要。😫 事前条件が増えている! ● ホテルBの場合:予約後に電報を送らないと完了しない。😫 事後条件が減っている! ● ホテルCの場合:実はスパ施設である。😫 不変条件が変わっている! ● ホテルDの場合:電話番号が080〜だとエラーで予約できない。😫 例外の種類が増えている! こんなホテル予約サイトは嫌だ プログラミングでも⼀緒だよね

Slide 33

Slide 33 text

事前条件が増えている例 class Car { public function start(): void { // 自動車を発進させる } }

Slide 34

Slide 34 text

事前条件が増えている例 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(); } }

Slide 35

Slide 35 text

事前条件が増えている例 class Driver { public function drive(Car $car): void { $car->start(); } } $driver = new Driver(); $driver->drive(new Car()); // 無事に発進できる $driver->drive(new ClassicCar()); // Error: 暖機運転が必要です! 😱

Slide 36

Slide 36 text

● LSP は継承を⽤いるべき条件を逆説的に説明している。 ● それって本当に継承を⽤いるべきですか? ● 継承ではなく委譲を選択するなど、継承は⽤法‧⽤量を守って利 ⽤しよう。 LSP/リスコフの置換原則:まとめ

Slide 37

Slide 37 text

leInterface Segregation Principle インターフェース分離の原則

Slide 38

Slide 38 text

Many client-specific interfaces are better than one general-purpose interface. 意訳:1つの汎⽤インターフェースがあるよりも、要求ごとに分離されたインターフェースが複数 あった⽅がいいよね。 ISP/インターフェース分離の原則

Slide 39

Slide 39 text

リポジトリインターフェースがあって…… interface Repository { public function lookup(int $id): Entity; public function store(Entity $entity): void; }

Slide 40

Slide 40 text

読み取り専⽤のリポジトリが現れた! 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'); } }

Slide 41

Slide 41 text

こうすれば安⼼だよね? // 読み取り可能なリポジトリのインターフェース 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 { // ... } }

Slide 42

Slide 42 text

● 全部⼊りの⼤きなインターフェースを⽤意するのではなく、⽤途 別に⼩さなインターフェースを複数⽤意した⽅が何かと便利。 ○ 実装の⼿間‧複雑さが減る。 ○ テスト⽤のダミーを⽤意するのも楽。 ● ただ割と後からでもどうにかなる部分ではある。必要に応じて後 から分離するという戦略でもいいと思っている。 ISP/インターフェース分離の原則:雑なまとめ

Slide 43

Slide 43 text

Dependency Inversion Principle 依存性逆転の原則

Slide 44

Slide 44 text

High-level modules should not import anything from low-level modules. Both should depend on abstractions, not concretions. 意訳:上位モジュールは下位からなにもインポートしてはならない。    さらに上位‧下位を問わず、実装ではなく抽象に依存しよう。 DIP/依存性逆転の原則

Slide 45

Slide 45 text

// コントローラー(上位=使う側)がサービス(下位=使われる側)に依存している final readonly class ItemController { public function get(int $id): Response { $service = new ItemService( /* * なんか沢山の引数 */ ); return $service->get($id); } } 上位が下位に依存している例

Slide 46

Slide 46 text

// 使う側で欲しいインターフェースを定義する 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 { // ... } 依存性を逆転させると……

Slide 47

Slide 47 text

● 上位が、下位の細かい事情に関与しない状態を作り、疎結合化、 関⼼の分離を実現する。 ● 下位の仕様変更などによって、上位のモジュールを変更する必要 をなくす。 ○ e.g. ファイルの保存先がローカルから S3 に変わった 何をしたい?

Slide 48

Slide 48 text

● 詳細な実装(下位のモジュール)に依存すると、仕様変更などの タイミングで修正範囲が広くなりがち。 ● DIP を守ることで、疎結合でメンテしやすい構造にすることがで きる。 ● とはいえ、全部ガチガチにやりすぎるとコストが⾼いのでどこま で真⾯⽬にやるのかは検討してもいいんじゃないかな? DIP/依存性逆転の原則:雑なまとめ

Slide 49

Slide 49 text

https://fortee.jp/phpcon-nagoya-2025/proposal/1b879fa8-1d32-49b9-9d6e-d013b1dcd76b

Slide 50

Slide 50 text

● SOLID 原則を守ることで、変更に強く、壊れにくい=品質の⾼い ソフトウェア開発に繋がる。 ● とはいえ SOLID 原則は銀の弾丸ではない。守ることが⽬的化して しまうと、かえって開発効率の低下を招くことも。 ● どこまで適⽤するか‧しないかを判断するためにも理解が⼤事。 さいごに:なぜ SOLID 原則を知るべきか