# Lesson 18: SOLID 實戰篇 (3)：完結LSP、ISP 與 魔王DIP

# 關於LSP

接下來進到L了，這裡的L指的就是里氏替換原則 (LSP , Liskov Substitution Principle)，這是許多後端工程師覺得最「抽象」的一個原則，但它其實是判斷「繼承 (Inheritance) 是否被濫用」最重要的標準。

## 什麼是里式替換 (LSP)？

> 「子類別 (Subclass) 必須能夠替換掉它們的父類別 (Base Class)，且程式的行為不會發生錯誤。」

簡單來說，如果程式碼依賴於一個父類別（或介面），那麼隨便塞一個該父類別的「子類別」進去，程式都應該要能正常運作，而不需要去修改呼叫端的程式碼。

## 鴨子測試 (The Duck Test)

如果它看起來像鴨子，叫聲像鴨子，但需要裝電池才能動，那它就是違反了 LSP。因為當你把這隻「機械鴨」混進真正的鴨群裡，原本預期鴨子會游泳的程式邏輯就會崩潰。

P.S.這裡的鴨子測試是延伸出來的應用。

## 常見訊號

在 Code Review 中，如果看到以下幾種情況，通常就是違反了 LSP：

1. **子類別拋出「未實作」例外**：父類別有某個方法，但子類別根本不需要，只好在方法裡 `throw new NotImplementedException()`。
    
2. 子類別方法是空的：為了滿足介面定義，子類別把方法留空不做事。
    
3. 呼叫端充滿了 `instanceof` 或類型檢查：呼叫端必須檢查「如果是 A 子類別，就做 X；如果是 B 子類別，就做 Y」。
    

## 實戰場景：支付閘道(Payment Gateway)的陷阱

假設我們正在開發一個電商系統的支付模組。我們有一個抽象的支付處理類別 `PaymentHandler`

### 違反LSP的設計

```php
abstract class PaymentHandler
{
    abstract public function processPayment(float $amount);
    abstract public function refundPayment(float $amount);
}

class PayPalHandler extends PaymentHandler
{
    public function processPayment(float $amount) { /* 呼叫 PayPal API 付款 */ }
    public function refundPayment(float $amount) { /* 呼叫 PayPal API 退款 */ }
}

// 問題來了：公司決定引入「紅利點數支付」
// 紅利點數可以折抵現金，但是「不能退款」（假設這是業務邏輯，點數扣了就不退）
class RewardsHandler extends PaymentHandler
{
    public function processPayment(float $amount) { /* 扣除使用者點數 */ }

    public function refundPayment(float $amount)
    {
        // 違反 LSP！子類別無法履行父類別的承諾
        throw new Exception("Rewards cannot be refunded.");
    }
}
```

### 修正方案：隔離與拆分

介面隔離 (Interface Segregation)，我們將「支付」與「退款」的行為分開。

```php
interface Payable
{
    public function processPayment(float $amount);
}

interface Refundable
{
    public function refundPayment(float $amount);
}

// PayPal 既可以付錢也可以退錢
class PayPalHandler implements Payable, Refundable
{
    public function processPayment(float $amount) { /* ... */ }
    public function refundPayment(float $amount) { /* ... */ }
}

// 紅利點數只實作 Payable
class RewardsHandler implements Payable
{
    public function processPayment(float $amount) { /* ... */ }
}
```

現在，編譯器或靜態分析工具會幫我們把關。如果一個支付方式不支援退款，根本無法將它傳入退款的邏輯中。

```php
class OrderService
{
    // 這裡 Type Hint 指定要 Refundable，所以絕對不會傳入 RewardsHandler
    public function refundOrder(Refundable $handler, float $amount)
    {
        $handler->refundPayment($amount);
    }
}
```

這樣一來，`RewardsHandler` 可以安心地當作 `Payable` 使用，而不會在需要 `Refundable` 的地方炸開。

## 契約設計 (Design by Contract)

LSP 在學術上還有兩個關於「契約」的重要規定，對於 API 設計很有幫助：

1. 前置條件 (Preconditions) 不能更強：
    
    * 父類別說：「給我大於 0 的數字我就能跑」。
        
    * 子類別不能說：「給我大於 100 的數字我才跑」。(你要求更多，呼叫端會無所適從)
        
2. 後置條件 (Postconditions) 不能更弱：
    
    * 父類別說：「我保證回傳一個有效的 JSON」。
        
    * 子類別不能說：「我可能會回傳 null 或空字串」。(你給的承諾變少了，呼叫端會處理錯誤)
        

## 補充說明 - 不是所有 override 都違反 LSP

里氏替換原則並不是禁止子類別覆寫 (override) 父類別的方法，它關心的是：

覆寫後，是否仍然遵守原本對呼叫端的「行為承諾」。

只要子類別的 override 沒有改變對外可觀察的行為契約，這樣的覆寫不但不違反 LSP，反而是常見且合理的設計。

### 合法、不違反 LSP 的 override 範例

```php
class CachedUserRepository extends UserRepository
{
    public function findById(int $id): User
    {
        // 先查 Cache
        if ($user = $this->cache->get($id)) {
            return $user;
        }

        // Cache miss，才走父類別邏輯
        $user = parent::findById($id);
        $this->cache->set($id, $user);

        return $user;
    }
}
/**
方法簽章沒變
回傳型別與例外行為沒變
呼叫端完全不需要知道「有沒有用快取」
*/
```

```php
class SecureFileStorage extends FileStorage
{
    public function save(string $path, string $content): void
    {
        // 子類別增加了加密行為
        $encrypted = $this->encrypt($content);

        parent::save($path, $encrypted);
    }
}
/**
父類別承諾的是「能成功儲存檔案」
子類別只是多做了一步處理
呼叫端不需要額外條件或調整使用方式
*/
```

## 總結

* 繼承是「Is-A」的關係：不僅僅是名字像，行為也要像。如果子類別要把父類別的功能「關掉」或「報錯」，那就不該繼承。
    
* LSP 是 OCP 的基礎：如果你違反了 LSP，呼叫端就必須檢查子類別類型，這就違反了 OCP (Open/Closed Principle)。
    
* 多用組合/介面，少用繼承：當你發現繼承層級很難符合 LSP 時，通常代表你應該改用介面組合 (Composition) 的方式來設計。
    

# 關於ISP

## 什麼是介面隔離原則

> Clients should not be forced to depend upon interfaces that they do not use.

與其設計一個無所不能的「胖介面」，不如把它拆分成多個特定的「瘦介面 (Thin Interface)」。

## 為什麼「胖介面」有壞味道

在後端開發中，我們經常會因為「貪圖方便」，把所有相關的方法都塞進同一個 Interface 裡。

### 全能的 `UserInterface`

```php
interface UserInterface {
    public function login();
    public function register();
    public function sendEmail(); // 為了寄送歡迎信
    public function generateReport(); // 為了給後台管理員看數據
}
```

**問題：**

* 不必要的依賴：負責「登入」的模組，為什麼要知道怎麼「產生報表」？
    
* 實作的痛苦：如果你只想做一個簡單的「訪客註冊」功能，你被迫要去實作 `generateReport()`（可能只好留空或丟例外，這又違反了 LSP）。
    
* 頻繁的修改：如果後台報表邏輯變了，導致 `generateReport` 簽章改變，連「登入模組」都要跟著重新編譯或測試，因為它們綁在同一個介面上。
    

## 實戰場景：工人與機器人

我們來看看ISP經典的教科書範例

### 違反 ISP 的設計

```php
interface Worker {
    public function work();
    public function eat();
}

class HumanWorker implements Worker {
    public function work() { /* 工作 */ }
    public function eat() { /* 吃便當 */ }
}

// 💥 問題來了：引進機器人
class RobotWorker implements Worker {
    public function work() { /* 工作 */ }
    
    public function eat() {
        // 機器人不用吃飯，但介面強迫我實作
        // 這裡通常會丟例外，或留空，這違反了 LSP
        throw new Exception("Robot does not eat!"); 
    }
}
```

### 遵守 ISP 的修正

```php
interface Workable {
    public function work();
}

interface Feedable {
    public function eat();
}

// 人類：既能工作也能吃
class HumanWorker implements Workable, Feedable {
    public function work() { /*...*/ }
    public function eat() { /*...*/ }
}

// 機器人：只能工作
class RobotWorker implements Workable {
    public function work() { /*...*/ }
}
```

## 補充說明 - 易混淆概念

還記得上面我們用於解決LSP的手法嗎？絕大多數因「介面過大」導致違反 LSP，解決方案正是 ISP。

LSP 是我們想要的「結果」，而 ISP 常常是達成這個結果的「方式」。

雖然 ISP 是解決 LSP 常見問題的解藥，但 LSP 的範圍比 ISP 更廣。有些 違反LSP的情況 ，不是靠拆分介面就能解決的。

### 範例：遵守 ISP 但違反 LSP

假設我們有一個銀行帳戶類別

```php
// 介面很乾淨，只有一個方法，完全符合 ISP
interface Account {
    /**
     * @return float 剩餘餘額 (保證大於等於 0)
     */
    public function withdraw(float $amount): float;
}

class NormalAccount implements Account {
    public function withdraw(float $amount): float {
        // 正常的提款邏輯
        return $this->balance - $amount;
    }
}

// 這個子類別也實作了同樣的介面，結構上沒問題
class VipAccount extends NormalAccount {
    public function withdraw(float $amount): float {
        // 💥 違反 LSP！
        // 父類別/介面承諾餘額 >= 0，但 VIP 可以透支
        // 這改變了行為的「後置條件 (Postcondition)」
        return $this->balance - $amount; // 可能回傳負數
    }
}
```

**分析**

1. ISP 沒問題：介面沒有多餘的方法，`withdraw` 是大家都需要的。
    
2. LSP 違反了：`VipAccount` 的行為破壞了 `Account` 的隱性契約（餘額不能為負）。呼叫端如果預期餘額永遠是正的，程式就會出錯。
    

### 結語

違反 ISP 高機率導致 違反 LSP

* 因為 ISP 造成了「胖介面」，強迫子類別去實作它做不到（或不該做）的方法。
    
* 結果：子類別被迫「造假」或「罷工」：
    
    1. 丟出例外 (`throw NotImplementedException`)
        
    2. 空實作 (方法裡什麼都不寫) 。
        

但違反 LSP 不一定違反 ISP

* 介面可能設計得非常完美、非常精簡（完全符合 ISP），但子類別的「實作邏輯」**或是**「繼承關係」本身就是錯的。
    

# 關於DIP

這就是SOLID的大魔王了，我都有點懷疑SOLID的順序是不是依照難易度排下來了。

DIP , Dependency inversion principle，依賴反轉原則。

## 什麼是依賴反轉原則？

DIP 有兩個繞口令的定義：

1. 高層模組不應該依賴低層模組，兩者都應該依賴於抽象。
    
2. 抽象不應該依賴於細節，細節應該依賴於抽象。
    

嗯…..是中文，但不懂…，還記得在L15有偷偷提到DIP嗎？那時候有提到：

DI 的核心原則是「依賴反轉 (DIP)」，「不要自己造工具，讓別人把工具傳進去。」

### 老闆與員工的關係

我們來看看一個生活化的比喻

沒有DIP：

老闆 想要做漢堡，老闆教員工A做漢堡，員工A離職，漢堡店陷入火海。

有DIP：

老闆 想要做漢堡，老闆製作了SOP，員工A針對SOP學會如何做漢堡，員工A離職，老闆找了員工B，員工B繼續快樂做漢堡。

## 為什麼叫 - 反轉

這裡我們就用L15的 `OrderService` 範例來進行說明，到底Inversion在哪裡

1. **正常控制流**：`OrderService` 呼叫 `GmailSender`。 (上 -&gt; 下)
    
2. **傳統依賴關係**：`OrderService use GmailSender`。 (上 -&gt; 下)
    
3. DIP 依賴關係：
    
    1. `OrderService` 依賴 `MailerInterface`。 (上 -&gt; 介面)
        
    2. `GmailSender`依賴 `MailerInterface`。 (下 -&gt; 介面)
        
    3. 依賴的箭頭方向，從「指向下方」變成了「指向介面」。
        

![反轉方向說明](https://cdn.hashnode.com/res/hashnode/image/upload/v1766480994792/20624c70-378b-4483-8046-9c3714d2e697.png align="center")

## 補充說明 - 易混淆概念

### 依賴注入與依賴反轉的關聯

依賴反轉是一個概念，而依賴注入是一種實作手法。

依賴反轉關心的是：類別之間的依賴與方向關係

依賴注入關心的是：物件如何被建立與賦值

### 有 DI 不代表有 DIP

```php
class OrderService {
    // 這裡依賴的是「具體」的 GmailService 類別
    public function __construct(GmailService $mailer) {
        $this->mailer = $mailer;
    }
}
```

Q：是不是 DI？

A：是，因為 GmailService 是注入進去，沒有在裡面 new。

Q：是不是 DIP？

A：不是，因為 OrderService (高層) 依然死死地依賴著 GmailService (低層實作)。如果 Gmail 換掉，OrderService 還是要改程式碼。依賴關係沒有「反轉」，箭頭還是從上指到下。

### 有 DI，也有 DIP

```php
class OrderService {
    // 這裡依賴的是「抽象」的 MailerInterface
    public function __construct(MailerInterface $mailer) {
        $this->mailer = $mailer;
    }
}
```

Q：是不是 DI？

A：是，因為 GmailService 是注入進去，沒有在裡面 new。

Q：是不是 DIP？

A：是，`OrderService` 現在只依賴介面。依賴的箭頭現在指向了 `MailerInterface` (抽象)，達成了依賴反轉。
