Slide 1

Slide 1 text

Shogo Kawase / @shogogg Jun. 28 2025 PHP Conference Japan 2025 PHP開発者のための SOLID原則再入門

Slide 2

Slide 2 text

SOLID原則を完全に理解する Today’s Goal

Slide 3

Slide 3 text

● SOLID原則を理解する ● SOLID原則がもたらす価値について理解する ● SOLID原則に則った設計ができるようになる ● SOLID原則を参照してコミュニケーションできるようになる Today’s Goal

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

なんだかむずかしい……

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

そんな人達のために……

Slide 8

Slide 8 text

PHPカンファレンス名古屋2025にて https://speakerdeck.com/shogogg/5fen-deli-jie-suru-solid-yuan-ze

Slide 9

Slide 9 text

5分はキツかった

Slide 10

Slide 10 text

今日は25分ある! (当社比5倍)

Slide 11

Slide 11 text

今日こそ終わらせる!

Slide 12

Slide 12 text

自己紹介 河瀨 翔吾 / Shogo Kawase 株式会社 PR TIMES ソフトウェアエンジニア(2024.12〜) I LOVE... 妻 / 型安全 / アジャイル / ももいろクローバーZ F1 / マリオカート shogogg shogogg

Slide 13

Slide 13 text

おしながき 1. SOLID原則がソフトウェア開発にもたらす価値と代償 2. 前提知識:DRY原則について 3. SOLID原則の5つの原則について 4. まとめ

Slide 14

Slide 14 text

おしながき 1. SOLID原則がソフトウェア開発にもたらす価値と代償 2. 前提知識:DRY原則について 3. SOLID原則の5つの原則について 4. まとめ

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

原則 ≠ 絶対的なルール ● 原則同士が対立し、トレードオフの関係になることも DRY/OAOO/SOLID を重視 → 設計が複雑化 → KISS原則違反 ● 原則は手段であって目的ではない すべての原則は何らかの価値を得るための手段。原則を守ることが目 的ではない。 ● 文脈によって価値の優先順位は変わる 「売れるかどうかわからない」場合と「数年間の運用・拡張が決まっ ている」場合では重視する価値が異なる

Slide 20

Slide 20 text

SOLID原則がソフトウェア開発にもたらす価値 ● 安定性・安全性 ソフトウェアを変更しても、障害が起きにくくなる ● 拡張性・変更容易性 仕様変更や機能追加に柔軟に対応できるようになる ● テスト容易性 モジュールごとのテストがしやすくなる ● 保守性・可読性 コードの責任と役割が明確・シンプルになり、開発が円滑化する

Slide 21

Slide 21 text

もちろん代償もある ● 設計・実装コストの増加 原則に則った設計・抽象化のため初期段階では時間や手間が掛かる ● 学習コストの増加 チームメンバーの習熟度によっては理解が難しくなる可能性がある ● さじ加減の難しさ やりすぎてしまうと逆に可読性やシンプルさを失ってしまうことも

Slide 22

Slide 22 text

しかし時は大AI時代

Slide 23

Slide 23 text

もちろん代償もある!……けども? ● 設計・実装コストの増加 原則に則った設計・抽象化のため初期段階では時間や手間が掛かる ● 学習コストの増加 チームメンバーの習熟度によっては理解が難しくなる可能性がある ● さじ加減の難しさ やりすぎてしまうと逆に可読性やシンプルさを失ってしまうことも ● ➡ AIを使えばここのコストは大幅に軽減できそう! ● ➡ この資料を使って説明すれば解決できるかも! ● ➡ こればかりは場数を踏んで慣れるしかない?

Slide 24

Slide 24 text

理解して使いこなそう!

Slide 25

Slide 25 text

おしながき 1. SOLID原則がソフトウェア開発にもたらす価値と代償 2. 前提知識:DRY原則について 3. SOLID原則の5つの原則について 4. まとめ

Slide 26

Slide 26 text

なぜここでDRY原則の話をするのか ● DRY原則は数ある原則の中でも特に有名なもの ● しかし誤解されていることがとっても多い ● SOLID原則を理解する上で、DRY原則の正しい理解は不可欠

Slide 27

Slide 27 text

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 より

Slide 28

Slide 28 text

Don’t Repeat Yourself DRYはソフトウェア開発の原則であり「あらゆる知識は、システム内に おいて単一で、曖昧さのない、信頼できる表現でなければならない 」 と述べています。 同じ情報を何度も繰り返し書かない ことで、コードはより保守しやすく なり、拡張しやすくなり、バグも少なくなります。 Getting Started with Rails の内容を翻訳

Slide 29

Slide 29 text

Don’t Repeat Yourself DRYはソフトウェア開発上の原則であり、「システムを構成する知識の あらゆる部品は、常に単一であり、明確であり、信頼できる形で表現 されていなければならない 」というものです。 同じコードを繰り返し書くことを徹底的に避ける ことで、コードが保守 しやすくなり、容易に拡張できるようになり、バグも減らせます。 Rails をはじめよう(公式日本語訳)より引用

Slide 30

Slide 30 text

Don’t Repeat Yourself DRYはソフトウェア開発上の原則であり、「システムを構成する知識の あらゆる部品は、常に単一であり、明確であり、信頼できる形で表現 されていなければならない 」というものです。 同じコードを繰り返し書くことを徹底的に避ける ことで、コードが保守 しやすくなり、容易に拡張できるようになり、バグも減らせます。 Rails をはじめよう(公式日本語訳)より引用 なぜか「情報」じゃなくて「コード」になっている……

Slide 31

Slide 31 text

DRY原則を正しく理解しよう ● 「コードの重複」ではなく 「知識の重複」を避ける ● 「○○に関する知識・処理はここにあり、他にはない」状態を作る ● 仕様とコードの関係がシンプルになり、変更しやすく、想定外のバグ を減らせる 💡Tips コードの重複を徹底的に避けるのは OAOO(Once And Only Once)原則

Slide 32

Slide 32 text

おしながき 1. SOLID原則がソフトウェア開発にもたらす価値と代償 2. 前提知識:DRY原則について 3. SOLID原則の5つの原則について 4. まとめ

Slide 33

Slide 33 text

1. Simple Responsibility Prinsiple 単一責任の原則

Slide 34

Slide 34 text

単一責任の原則 “There should never be more than one reason for a class to change.” 意訳: クラスを変更する理由が複数あってはならない。

Slide 35

Slide 35 text

単一責任の原則:わかりやすく言うと? ● ひとつのクラスや関数に機能をいくつも詰め込まない ● 既にあるクラスや関数を軽率に使い回さない DRY原則を勘違いしているとやってしまいがち

Slide 36

Slide 36 text

アンチパターン例①

Slide 37

Slide 37 text

// 商品を注文する function makeOrder(array $order): void { $this->insertRecord($order); $this->makePayment($order); $this->sendmail($order); } ECサイトの注文処理ができたぞ!

Slide 38

Slide 38 text

// 商品を注文する 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); } 予約に対応?任せとけ!!

Slide 39

Slide 39 text

// 商品を注文する 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); } 抽選機能?余裕だぜ!!

Slide 40

Slide 40 text

// 商品を注文する 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); } 特別な条件の販売にも対応するぞ!

Slide 41

Slide 41 text

// 商品を注文する 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); } テストを書くのがしんどい……

Slide 42

Slide 42 text

アンチパターン解説①:機能を詰め込む ● コードが複雑になる ➡ 可読性・保守性が低下 ● テストが大変になる ➡ テストコードの可読性・保守性も低下

Slide 43

Slide 43 text

アンチパターン例②

Slide 44

Slide 44 text

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を実装したよ!

Slide 45

Slide 45 text

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原則だ!流用するぞー!

Slide 46

Slide 46 text

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仕様が変わったから 注文履歴はレスポンスから消すよ

Slide 47

Slide 47 text

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 を呼び出してごにょごにょする } } 何もしてないのに壊れた……

Slide 48

Slide 48 text

アンチパターン解説②:用途外の流用 ● 予想外の範囲に影響する ➡ 障害の可能性UP ● 影響範囲が広がりすぎる ➡ バグ修正・機能追加の工数増加 ● 怖くて修正できなくなる ➡ 技術的負債の増加

Slide 49

Slide 49 text

単一責任の原則:まとめ 以下のアンチパターンを避け、 モジュールの責務をシンプルに保とう ● ひとつのクラスや関数に機能をいくつも詰め込まない ● 既にあるクラスや関数を軽率に使い回さない

Slide 50

Slide 50 text

2. Open/Closed Prinsiple 開放閉鎖の原則

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

開放閉鎖の原則:わかりやすく言うと? ソフトウェアに機能を追加する際に…… ● 修正に対して閉じている = 既存のコードを変更せずに ● 拡張に対して開いている = 新しいコードの追加で対応できる ように設計しましょう、ということ。

Slide 54

Slide 54 text

アンチパターン例①

Slide 55

Slide 55 text

// 商品を注文する 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); }

Slide 56

Slide 56 text

● 通常・予約・抽選という複数の注文方法に1つのコードで対応している ● さらに特殊な条件の抽選という条件が加わり、条件分岐だらけ に ● コードが複雑になり、可読性・保守性が低下 ● テストが大変になり、テストコードの可読性・保守性も低下 アンチパターン例①:解説

Slide 57

Slide 57 text

解消例 (Strategy Pattern)

Slide 58

Slide 58 text

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); }

Slide 59

Slide 59 text

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); } 注文種別ごとの戦略を定義するインターフェース

Slide 60

Slide 60 text

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); } 注文種別ごとに異なる実装(クラス)を定義する

Slide 61

Slide 61 text

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); } 戦略を選択するためのファクトリークラスを実装

Slide 62

Slide 62 text

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); } 注文処理からは適切な戦略を呼び出す

Slide 63

Slide 63 text

● 似て非なる注文処理=戦略をインターフェース化し、注文方法ごとに 独立したクラスとして定義する ● 新しい注文方法が増えた場合、StrategyInterface を継承する新しい クラスの追加のみで対応可能 となる ● 新しい注文方法が増えても、既存のクラスを修正する必要がないため 既存機能で不具合が起きる可能性がぐっと低くなる OCP適用例:ストラテジーパターン

Slide 64

Slide 64 text

アンチパターン例②

Slide 65

Slide 65 text

// 消費税を表す列挙型 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), }; } }

Slide 66

Slide 66 text

// 消費税を表す列挙型 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 式を使って計算している

Slide 67

Slide 67 text

// 消費税を表す列挙型 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), }; } } 消費税率に「軽減税率」が増えた!

Slide 68

Slide 68 text

// 消費税を表す列挙型 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 に軽減税率が渡されるとエラーが起きる!

Slide 69

Slide 69 text

// 消費税を表す列挙型 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 式の条件を増やす必要がある

Slide 70

Slide 70 text

● パターンが増える度に条件分岐が増え、既存コードに修正が発生する ➡ 影響範囲・対応コストが増える ➡ 不具合のリスク・テストの負荷が増える アンチパターン例②:解説 もし商品クラス以外にも消費税計算があったら……?

Slide 71

Slide 71 text

解消例

Slide 72

Slide 72 text

// 商品クラス 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; }

Slide 73

Slide 73 text

// 商品クラス 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; } 消費税を表す型をインターフェースや抽象クラスとして定義

Slide 74

Slide 74 text

// 商品クラス 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; } 税額の計算は消費税クラスに実装する

Slide 75

Slide 75 text

// 商品クラス 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; } 税率ごとにサブクラスを定義する

Slide 76

Slide 76 text

// 商品クラス 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; } 商品クラスは渡された税率のメソッドを呼び出すだけ

Slide 77

Slide 77 text

// 商品クラス 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; } 新しい税率は新しいクラスを定義

Slide 78

Slide 78 text

// 商品クラス 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; } 新しい税率は新しいクラスを定義 既存の商品クラスは変更せずに機能追加ができた!

Slide 79

Slide 79 text

● シンプルな処理だから、と安直に if 文や switch 文、match 式を書く のではなく、知識・情報を1ヶ所にまとめるよう抽象化する ことで、 自然と OCP に沿った設計となる ● でも PHP だと、この規模のコードをここまで徹底してクラスに分ける ことで逆に可読性や保守性を下げる場合もある のでは……? OCP適用例:DRY原則を守る

Slide 80

Slide 80 text

// 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; } }

Slide 81

Slide 81 text

// 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); }

Slide 82

Slide 82 text

// 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 }

Slide 83

Slide 83 text

// 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() }

Slide 84

Slide 84 text

解消例 (PHP向け妥協版)

Slide 85

Slide 85 text

// 商品クラス 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), }; } } 商品クラスは渡された税率のメソッドを呼び出すだけ

Slide 86

Slide 86 text

// 商品クラス 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 式等で記述

Slide 87

Slide 87 text

// 商品クラス 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), }; } } 常に列挙子を網羅するテストを書くこと!

Slide 88

Slide 88 text

開放閉鎖の原則:まとめ 高い拡張性と保守性を両立するため コードの「修正」ではなく「追 加」によって機能が拡張できる ように設計しよう ● ストラテジーパターン やDRY原則に沿った設計 がポイント ● 将来的な拡張が予想される部分で適用 すると威力を発揮する 例)消費税率、配送方法、支払方法、etc... ● 「区分」「種類」等での if/switch/match が出てきたら OCP を 適用できないか検討してみよう ● PHP でやるときは可読性・保守性とのバランスも意識したい

Slide 89

Slide 89 text

3. Liskov Substitution Prinsiple リスコフの置換原則

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

Photo: Kenneth C. Zirkel, CC BY-SA 3.0, via Wikimedia Commons Barbara Liskov (1939.11 〜) ● 米国で女性初の博士(Computer Science) ● MIT教授(Computer Science) ● 2008年にチューリング賞を受賞

Slide 92

Slide 92 text

リスコフの置換原則:より具体的に言うと? ● メソッドの事前条件を増やさない ● メソッドの事後条件を減らさない ● オブジェクトの不変条件を変えない ● メソッドが投げる例外の種類 を増やさない

Slide 93

Slide 93 text

例:こんなホテル予約サイトはイヤだ ● ホテルAの場合:予約前に電話で確認が必要 ● ホテルBの場合:予約後に電報を送らないと予約が完了しない ● ホテルCの場合:当日行ってみたらホテルじゃなくてスパだった ● ホテルDの場合:施設側の選り好みで予約不可の場合がある

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

メソッドの事前条件を増やさない

Slide 97

Slide 97 text

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!

Slide 98

Slide 98 text

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 がないと例外を投げている

Slide 99

Slide 99 text

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 の実装が何かを知らずに使いたい

Slide 100

Slide 100 text

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 を省略しても動く

Slide 101

Slide 101 text

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 に差し替えると例外発生!

Slide 102

Slide 102 text

メソッドの事前条件を増やさない メソッドを呼び出す際に呼び出し元が満たすべき条件を増やさない ● 入力の制約 引数の型、引数のバリデーション条件 etc... ● オブジェクト自身の状態 事前に特定のメソッドを呼び出す必要がある etc... ● 外部リソースの状態 トランザクションが開始している、商品の在庫がある etc...

Slide 103

Slide 103 text

メソッドの事前条件を増やさない メソッドを呼び出す際に呼び出し元が満たすべき条件を増やさない ● 入力の制約 引数の型、引数のバリデーション条件 etc... ● オブジェクト自身の状態 事前に特定のメソッドを呼び出す必要がある etc... ● 外部リソースの状態 トランザクションが開始している、商品の在庫がある etc... Factory パターンや DI で解決できる場合もある

Slide 104

Slide 104 text

メソッドの事後条件を減らさない

Slide 105

Slide 105 text

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 ...

Slide 106

Slide 106 text

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 ... ファイルサイズ・作成日時・パーミッションを返す

Slide 107

Slide 107 text

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 にはパーミッションがないから、と戻り値の要素を減らしている

Slide 108

Slide 108 text

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 の実装が何かを知らずに使いたい

Slide 109

Slide 109 text

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 を使えばちゃんと動く

Slide 110

Slide 110 text

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 に差し替えるとエラーが発生!

Slide 111

Slide 111 text

メソッドの事後条件を減らさない メソッドの処理完了時に保証する状態や結果(約束)を守る ● 戻り値 戻り値の型や、その内容、範囲 etc... ● オブジェクト自身の状態 メソッド実行後にあるべきオブジェクトの状態 ● 外部リソースの状態や副作用 オブジェクトが永続化される、メールが送信される etc...

Slide 112

Slide 112 text

オブジェクトの不変条件を変えない

Slide 113

Slide 113 text

オブジェクトの不変条件を変えない オブジェクトが常に満たすべき条件・ルールを変えてはならない ● オブジェクト自身の状態やメソッドの戻り値 例1)$item->price は常に値を持ち、0以上である 例2)$member->email の値は常にメールアドレスである 例3)$date->getMonth() の戻り値は常に 1〜12 である

Slide 114

Slide 114 text

メソッドが投げる例外の種類を増やさない

Slide 115

Slide 115 text

メソッドが投げる例外の種類を増やさない 呼び出し元が予期しない例外を投げてはならない ● 文字通りの意味 例)S3 実装では SDK が投げる AWSException をそのまま伝搬する ちゃんと共通の例外で wrap してから投げよう

Slide 116

Slide 116 text

リスコフの置換原則:まとめ 継承(インターフェースの実装を含む)時に守るべきルール を徹底す ることで、拡張しやすく、壊れにくい状態を維持しよう ● クライアント(オブジェクトやメソッドを使う側)との約束を守る ● コードの再利用目的での安易な継承は避ける ● 違反しそうになったら委譲など継承以外の方法を検討する

Slide 117

Slide 117 text

4. Interface Segregation Prinsiple インターフェース分離の原則

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

アンチパターン例

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

// リポジトリインターフェース 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'); } } 読み取り専用のリポジトリが必要になった

Slide 122

Slide 122 text

// リポジトリインターフェース 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'); } } メソッドの定義が必要なので仕方なく例外を投げる……

Slide 123

Slide 123 text

解消例

Slide 124

Slide 124 text

// 読み取り可能なリポジトリのインターフェース 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 { // ... } }

Slide 125

Slide 125 text

// 読み取り可能なリポジトリのインターフェース 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 { // ... } } 用途別に細かくインターフェースを定義する

Slide 126

Slide 126 text

// 読み取り可能なリポジトリのインターフェース 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 { // ... } } 必要なインターフェースを複数実装する

Slide 127

Slide 127 text

// 読み取り可能なリポジトリのインターフェース 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 { // ... } } 余分なメソッドの定義がなくなってスッキリ!

Slide 128

Slide 128 text

インターフェース分離の原則:まとめ 全部入りの大きなインターフェースを1つだけ用意するのではなく、 用途別の小さなインターフェースに分けて定義しよう ● 余分な実装やテスト を書かなくて済むようになる ● テスト用のダミーを用意するのも楽になる ● 割と後からでもどうにかなる ● 必要に応じて後から分割する戦略 でもあまり困らない。序盤から無 理に適用しなくてもいいのでは?

Slide 129

Slide 129 text

5. Dependency Inversion Prinsiple 依存性逆転の原則

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

● 上位モジュールは下位からなにもインポートしてはならない ➡ 上位(何をするのか=ビジネスロジック・ドメインモデル)から   下位(どうやるのか=インフラ層・WEB 境界など)に依存しない ● さらに上位・下位を問わず、実装ではなく抽象に依存すべきである ➡ 具体的な実装ではなく「何をしたいのか」という抽象化を行って   そこに依存する 依存性逆転の原則:わかりやすく言うと? 逆転云々はいったん忘れてもOK

Slide 132

Slide 132 text

● 関心の分離と疎結合化 が促進され、タスクの分割や分業、単体テスト がしやすい設計となる ● フレームワークやライブラリ、外部 API の仕様やインフラ構成などの 外部依存とビジネスロジックが分離 されることで外部環境の変化に強 い設計となる 依存性逆転の法則:なにがうれしいの?

Slide 133

Slide 133 text

● 依存性の注入(DI) コードから直接依存して new するのではなく、外部から依存を注入 す ることで疎結合となり、関心が分離できる ● 腐敗防止層の導入 フレームワークやライブラリを直接呼び出すのではなく、使う側の関心 に合わせて腐敗防止層 を導入し、技術的詳細の変化に対応しやすくする ● Clean Architecture DIP の権化とも言えるアーキテクチャなので、クリーンアーキテクチャ に沿うように設計すれば自然と DIP にも沿った設計になる(はず) 依存性逆転の法則:どのように実現するの?

Slide 134

Slide 134 text

依存性逆転の原則:下位から上位へ依存する Database Adapters Use Cases Entity Gateway API Controller Web Framework/Library

Slide 135

Slide 135 text

アンチパターン例

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

解消例

Slide 141

Slide 141 text

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); // その他アップロードに関する処理 ... } }

Slide 142

Slide 142 text

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); // その他アップロードに関する処理 ... } } ファイルを保存したい、というビジネス側の都合で インターフェースを定義する

Slide 143

Slide 143 text

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); // その他アップロードに関する処理 ... } } ビジネス側の都合に合わせて技術的な詳細を実装する

Slide 144

Slide 144 text

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); // その他アップロードに関する処理 ... } } ビジネスロジックから技術的な詳細への依存が消えた

Slide 145

Slide 145 text

依存性逆転の原則:まとめ 上位(抽象的で安定したビジネスのコア)でインターフェースを定義し、 下位(技術的な詳細)が依存する形にすることで 変化に強い設計にしよう ● 開発の分業や単体テストが楽になる ● フレームワーク・ライブラリ・外部 API・インフラ構成など外部環境の 変更に対応しやすくなる ● 依存性の注入(DI) や腐敗防止層 などを活用する ● 開発初期から全部真面目にやりすぎるとコストが高すぎるかも?

Slide 146

Slide 146 text

おしながき 1. SOLID原則がソフトウェア開発にもたらす価値と代償 2. 前提知識:DRY原則について 3. SOLID原則の5つの原則について 4. まとめ

Slide 147

Slide 147 text

まとめ ● SOLID原則を守って設計することで高い安全性・安定性・保守性・ 可読性・変更容易性・テスト容易性 を手に入れることができる ● ただし、あらゆる文脈で常に正解となる銀の弾丸ではない ● SOLID原則を守ることは目的ではなく、価値を得るための手段 ● SOLID原則を知らずに技術的負債を生むのではなく、理解した上で ビジネス上の価値を生むために取捨選択 することが重要

Slide 148

Slide 148 text

THANK YOU!!

Slide 149

Slide 149 text

最後に宣伝

Slide 150

Slide 150 text

We are Hiring!!