Share
Embed
5分で理解する SOLID 原則 #phpcon_nagoya
PHPカンファレンス名古屋2025 LT 登壇資料です
UPDATE [2025-02-26]
・当日行った編集を一部反映しました。
・フォントの崩れを解消しました。
shogogg
February 21, 2025
Transcript
50 slides
-
5分で理解する SOLID 原則 Feb. 22 2025 PHP Conference 名古屋 2025
Shogo Kawase / @shogogg -
⾃⼰紹介 河瀨 翔吾 / Shogo Kawase 株式会社 PR TIMES ソフトウェアエンジニア(2024.12〜)
シン‧アジャイル コミュニティ運営メンバー 好きな⾔葉 型安全 / アジャイル 好きなアイドル ももいろクローバーZ shogogg shogogg -
• Simple Responsibility Principle - 単⼀責任の原則 • Open/Closed Principle -
開放閉鎖の原則 • Liskov Substitution Principle - リスコフの置換原則 • Interface Segregation Principle - インターフェース分離の原則 • Dependency Inversion Principle - 依存性逆転の原則 SOLID 原則とは -
There should never be more than one reason for a
class to change. 超意訳:クラスを変更する理由は1つだけにしよう。 SRP/単⼀責任の原則 -
例えば、こんなクラスがあったとき…… // ユーザー情報取得 API 向けに用意したサービス final readonly class GetUserDataService {
public function get(int $userId): array { // API の仕様に基づき, ユーザーの情報だけでなく注文履歴も返す . return [ 'user' => ..., 'orders' => ..., ]; } } -
後⽇、前述のサービスを修正するときに…… • 予期せぬ箇所で問題が起こるかも? ➡ 品質の低下 • 影響範囲が増え、検証範囲が増える ➡ ⼯数の増加 •
怖くて既存のコードが修正しづらい ➡ 技術的負債の増加 これを許すとどうなる? -
Software entities (classes, modules, functions, etc.) should be open for
extension, but closed for modification. 意訳:クラスや関数は拡張に対して開かれ、修正に対して閉じているべきである。 OCP/開放閉鎖の原則 -
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), }; } } 消費税計算の例 -
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), }; } } 軽減税率が増えると……? -
軽減税率が増えると……? 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), }; } } -
マジメな解消例 // 税率側に計算するメソッドを定義する 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); } } -
妥協的な解消例 // 税率側に計算するメソッドを定義する 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); } } -
• 機能を追加‧変更する際あちこち修正しないで済むように! ◦ 改修コストを抑える ◦ 修正漏れを防ぐ • 区分値での if ⽂や
switch ⽂が出てきたら要注意! • OCP に違反すると DRY 原則にも違反しがち。 OCP/開放閉鎖の原則:まとめ -
Functions that use pointers or references to base classes must
be able to use objects of derived classes without knowing it. 意訳:とあるクラスを扱う関数が、その派⽣クラスについて知らなくても扱えるようにしよう。 LSP/リスコフの置換原則 -
リスコフ is 誰? バーバラ‧リスコフ Photo: Kenneth C. Zirkel, CC BY-SA
3.0, via Wikimedia Commons -
• ホテルAの場合:予約前に電話で確認が必要。 事前条件が増えている!😫 • ホテルBの場合:予約後に電報を送らないと完了しない。 事後条件が減っている!😫 • ホテルCの場合:実はスパ施設である。 不変条件が変わっている!😫 •
ホテルDの場合:電話番号が080〜だとエラーで予約できない。 例外の種類が増えている!😫 こんなホテル予約サイトは嫌だ -
• ホテルAの場合:予約前に電話で確認が必要。😫 事前条件が増えている! • ホテルBの場合:予約後に電報を送らないと完了しない。😫 事後条件が減っている! • ホテルCの場合:実はスパ施設である。😫 不変条件が変わっている! •
ホテルDの場合:電話番号が080〜だとエラーで予約できない。😫 例外の種類が増えている! こんなホテル予約サイトは嫌だ プログラミングでも⼀緒だよね -
事前条件が増えている例 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(); } } -
事前条件が増えている例 class Driver { public function drive(Car $car): void {
$car->start(); } } $driver = new Driver(); $driver->drive(new Car()); // 無事に発進できる $driver->drive(new ClassicCar()); // Error: 暖機運転が必要です! 😱 -
リポジトリインターフェースがあって…… interface Repository { public function lookup(int $id): Entity; public
function store(Entity $entity): void; } -
読み取り専⽤のリポジトリが現れた! 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'); } } -
こうすれば安⼼だよね? // 読み取り可能なリポジトリのインターフェース 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 { // ... } } -
High-level modules should not import anything from low-level modules. Both
should depend on abstractions, not concretions. 意訳:上位モジュールは下位からなにもインポートしてはならない。 さらに上位‧下位を問わず、実装ではなく抽象に依存しよう。 DIP/依存性逆転の原則 -
// コントローラー(上位=使う側)がサービス(下位=使われる側)に依存している final readonly class ItemController { public function get(int
$id): Response { $service = new ItemService( /* * なんか沢山の引数 */ ); return $service->get($id); } } 上位が下位に依存している例 -
// 使う側で欲しいインターフェースを定義する 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 { // ... } 依存性を逆転させると…… -
• SOLID 原則を守ることで、変更に強く、壊れにくい=品質の⾼い ソフトウェア開発に繋がる。 • とはいえ SOLID 原則は銀の弾丸ではない。守ることが⽬的化して しまうと、かえって開発効率の低下を招くことも。 •
どこまで適⽤するか‧しないかを判断するためにも理解が⼤事。 さいごに:なぜ SOLID 原則を知るべきか