# Lesson 14: 物件導向的靈魂-介面與抽象類別

很多剛轉職或自學的工程師，最討厭看到的兩個關鍵字就是 `interface` 和 `abstract`。

心中一定有過這個疑問：「為什麼要寫一個『空的』函式放在那邊？直接把程式碼寫在 Class 裡面不好嗎？」

這堂課我們要來解開這個誤會。這不是為了增加程式碼行數，而是為了 「多型 (Polymorphism)」 與 「抽換 (Swap)」 的彈性。

# 繼承的盲點：為什麼不能只用 `extends`

假設我們在做一個「物流系統」，有「黑貓 (BlackCat)」和「新竹物流 (HCT)」。 直覺寫法通常是這樣：

```php
class BlackCat {
    public function ship($parcel) {
        return "黑貓已運送包裹: " . $parcel;
    }
}

class HCT {
    public function ship($parcel) {
        return "新竹物流已運送包裹: " . $parcel;
    }
}
```

Service中可能就長這樣

```php
// 如果今天是黑貓
$logistic = new BlackCat();
$logistic->ship('iPhone 15');

// 如果明天老闆說要換成新竹物流
// 你得把所有用到 BlackCat 的地方，全部改成 HCT
// 萬一 HCT 的方法不叫 ship() 叫做 send() 怎麼辦？這就是災難。
```

這時候，我們需要「規範」。

# 介面 ：定義「行為的合約」

**I**nterface 像是一個 USB 插孔的規格。 電腦不在乎插進來的是滑鼠、鍵盤還是隨身碟，它只在乎「你插得進去」且「你可以運作」。

* Interface 只管 「你能做什麼」，不管你 「你是誰」。
    
* Interface是一種型別契約，他只包含方法的簽名，沒有實現邏輯。
    
* 一個類別可以實現多個Interface，但必須實現所有Interface中定義的方法
    
* Interface會強制實現某些方式，確保類別具有特定的行為，使用Interface可以更加清晰的定義類別的職責
    

> 注：方法的簽名：指方法的 名稱、參數列表，以及（若有）回傳型別，不包含具體實作邏輯。

## 實作 Interface

我們定義一個物流介面，規定大家都必須有一個 `ship` 方法：

```php
interface LogisticInterface {
    // 介面裡的方法不能有內容，只能定義名稱跟參數
    public function ship($parcel);
}
```

接著讓兩家物流公司都 **遵守implements**：

```php
class BlackCat implements LogisticInterface {
    public function ship($parcel) {
        // 實作黑貓的邏輯
        return "黑貓運送: " . $parcel;
    }
}

class HCT implements LogisticInterface {
    public function ship($parcel) {
        // 實作新竹物流的邏輯
        return "新竹物流運送: " . $parcel;
    }
}
```

```php
function processOrder(LogisticInterface $logistic, $parcel) {
    // 我不在乎 $logistic 是黑貓還是新竹物流
    // 我只知道它一定有 ship() 方法，因為它簽了合約 (Interface)
    $logistic->ship($parcel);
}
```

這就是 **多型 (Polymorphism)** 的威力。

# 抽象類別 Abstract Class：提取「共同的邏輯」

那 `Abstract Class` 又是什麼？

如果 `Interface` 是為了規範「外部行為」，那 `Abstract Class` 就是為了 **「減少內部重複的程式碼」**。

* 抽象類別無法被實例化，只能用於繼承
    
* 抽象類別可以包含一般function跟抽象function
    
* 抽象方法不包含具體邏輯，只包含方法的簽名，如果抽象類別宣告了抽象方法，則子類別必須實作出該方法（或一樣使用方法簽名）
    

假設黑貓和新竹物流，雖然運送方式不同，但 **「計算運費」** 的公式是一樣的（例如都是 重量 \* 10）。我們不想在兩個 Class 裡寫兩遍一樣的公式。

## 實作 Abstract Class

Abstract Class 就像是一個 **「未完成的藍圖」**。**它不能被直接** `new` 出來，只能被繼承。

```php
abstract class BaseLogistic implements LogisticInterface {
    // 1. 具體的方法：大家共用的邏輯寫在這裡
    public function calculateFee($weight) {
        return $weight * 10;
    }

    // 2. 抽象的方法：強迫子類別一定要去實作 (跟 Interface 很像)
    // 因為每家送貨方式不同，這裡先空著留給子類別去寫
    abstract public function ship($parcel);
}
```

現在，我們的物流公司可以這樣寫：

```php
class BlackCat extends BaseLogistic {
    public function ship($parcel) {
        return "黑貓運送";
    }
}
```

`BlackCat` 現在自動擁有了 `calculateFee` 的能力，同時被迫實作了 `ship` 的行為。

| 特性 | Interface (介面) | Abstract Class (抽象類別) |
| --- | --- | --- |
| 核心概念 | 行為規範 (Contract) | 部分實作與共用邏輯 |
| 比喻 | USB 插槽、合約 | 車子的底盤、未完成的藍圖 |
| 實作內容 | 完全不能寫程式碼邏輯 (純定義) | 可以寫共用的程式碼，也可以留空 (abstract method) |
| 繼承數量 | 可以多重實作 (implements A, B, C) | 只能單一繼承 (extends A) |
| 使用時機 | 當你希望不同的類別都有相同的方法名稱時 (如：Log, Cache, Mail) | 當你有多個類別長得很像，有很多重複程式碼要共用時 |

# 實戰中的選擇

在「系統架構設計」與「依賴反轉（DIP）」層面，Interface 的重要性通常高於 Abstract Class。

為什麼？因為我們通常更在乎「這個物件能不能被抽換」，而不是「繼承層級」。

* **優先考慮 Interface**：為了未來的擴充性 (例如 Lesson 15 要講的依賴注入)。
    
* **輔助使用 Abstract**：當你發現多個實作類別有大量重複程式碼時，再用 Abstract Class 來重構 (Refactor)。
    

## 為什麼不要濫用 Abstract Class

Abstract Class 看起來很美好：

* 可以寫共用邏輯
    
* 可以少寫重複程式碼
    
* 子類別看起來「很整齊」
    

但在真實專案中，**濫用 Abstract Class 往往是系統開始變硬、變脆的起點**。

下面是三個最常見、也最容易被忽略的風險。

### 抽象類別會「過早凍結設計」

當你建立一個 Abstract Class 時，你其實是在對未來下這樣的假設：

> 「這些子類別永遠都會長得差不多。」

這個假設在一開始幾乎一定是錯的。

**問題場景**

```php
abstract class BaseLogistic {
    public function calculateFee($weight) {
        return $weight * 10;
    }

    abstract public function ship($parcel);
}

```

一開始很合理，但半年後需求來了：

* DHL：計費方式不同（體積重）
    
* Uber Eats：即時動態加價
    
* 自取：沒有運費
    

你會開始在 `BaseLogistic` 裡面看到：

```php
if ($this instanceof DHL) { ... }
if ($this instanceof UberEats) { ... }
```

這是一個明確的設計退化訊號。

Abstract Class 讓我們在還不了解變化之前，就把邏輯定死了。

### **單一繼承會快速限制擴充性（PHP 的現實）**

在 PHP 中：

* Class 只能繼承一個 Abstract Class
    
* 但可以實作 多個 Interface
    

這代表什麼？

如果用了 Abstract Class

```php
class BlackCat extends BaseLogistic
```

就失去了：

```php
class BlackCat extends BaseLogistic, Trackable, Refundable // 不能
```

只能選擇：

* 再塞更多功能進 `BaseLogistic`
    
* 或開始用 Trait 補洞（通常會更亂）
    

**相對地，Interface 不會有這個問題**

```php
class BlackCat implements LogisticInterface, TrackableInterface, RefundableInterface
```

**I**Interface 通常搭配「組合（Composition）」使用，而 Abstract Class 屬於「繼承（Inheritance）」模型。

而現代設計更偏好前者。

### Abstract 容易變成「上帝類別」（God Class）

當團隊開始「為了共用而共用」：

> 「反正大家都會用到，放在 Base 裡面就好。」

Abstract Class 很容易變成這樣：

```php
abstract class BaseLogistic {
    public function calculateFee() {}
    public function logShipment() {}
    public function notifyUser() {}
    public function retryPolicy() {}
    public function audit() {}
}
```

結果是：

* 子類別被迫「繼承一堆用不到的東西」
    
* 修改 Base Class 影響所有子類別
    
* 測試成本指數型上升
    

這直接違反了：

* SRP（單一職責原則）
    
* OCP（開放封閉原則）
    

### 實戰建議

設計初期：

* 先用 Interface 定義「能做什麼」
    
* 用組合（Service / Strategy）解決差異
    

重構階段：

* 當看到「多個實作類別真的有穩定重複邏輯」
    
* 再抽出一個「小而薄」的 Abstract Class
    

### 總結

Interface 是為了「未來的變化」， Abstract Class 是為了「已經確定不會變的部分」。
