Lesson 14: 物件導向的靈魂-介面與抽象類別
很多剛轉職或自學的工程師,最討厭看到的兩個關鍵字就是 interface 和 abstract。
心中一定有過這個疑問:「為什麼要寫一個『空的』函式放在那邊?直接把程式碼寫在 Class 裡面不好嗎?」
這堂課我們要來解開這個誤會。這不是為了增加程式碼行數,而是為了 「多型 (Polymorphism)」 與 「抽換 (Swap)」 的彈性。
繼承的盲點:為什麼不能只用 extends
假設我們在做一個「物流系統」,有「黑貓 (BlackCat)」和「新竹物流 (HCT)」。 直覺寫法通常是這樣:
class BlackCat {
public function ship($parcel) {
return "黑貓已運送包裹: " . $parcel;
}
}
class HCT {
public function ship($parcel) {
return "新竹物流已運送包裹: " . $parcel;
}
}
Service中可能就長這樣
// 如果今天是黑貓
$logistic = new BlackCat();
$logistic->ship('iPhone 15');
// 如果明天老闆說要換成新竹物流
// 你得把所有用到 BlackCat 的地方,全部改成 HCT
// 萬一 HCT 的方法不叫 ship() 叫做 send() 怎麼辦?這就是災難。
這時候,我們需要「規範」。
介面 :定義「行為的合約」
Interface 像是一個 USB 插孔的規格。 電腦不在乎插進來的是滑鼠、鍵盤還是隨身碟,它只在乎「你插得進去」且「你可以運作」。
Interface 只管 「你能做什麼」,不管你 「你是誰」。
Interface是一種型別契約,他只包含方法的簽名,沒有實現邏輯。
一個類別可以實現多個Interface,但必須實現所有Interface中定義的方法
Interface會強制實現某些方式,確保類別具有特定的行為,使用Interface可以更加清晰的定義類別的職責
注:方法的簽名:指方法的 名稱、參數列表,以及(若有)回傳型別,不包含具體實作邏輯。
實作 Interface
我們定義一個物流介面,規定大家都必須有一個 ship 方法:
interface LogisticInterface {
// 介面裡的方法不能有內容,只能定義名稱跟參數
public function ship($parcel);
}
接著讓兩家物流公司都 遵守implements:
class BlackCat implements LogisticInterface {
public function ship($parcel) {
// 實作黑貓的邏輯
return "黑貓運送: " . $parcel;
}
}
class HCT implements LogisticInterface {
public function ship($parcel) {
// 實作新竹物流的邏輯
return "新竹物流運送: " . $parcel;
}
}
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 出來,只能被繼承。
abstract class BaseLogistic implements LogisticInterface {
// 1. 具體的方法:大家共用的邏輯寫在這裡
public function calculateFee($weight) {
return $weight * 10;
}
// 2. 抽象的方法:強迫子類別一定要去實作 (跟 Interface 很像)
// 因為每家送貨方式不同,這裡先空著留給子類別去寫
abstract public function ship($parcel);
}
現在,我們的物流公司可以這樣寫:
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 時,你其實是在對未來下這樣的假設:
「這些子類別永遠都會長得差不多。」
這個假設在一開始幾乎一定是錯的。
問題場景
abstract class BaseLogistic {
public function calculateFee($weight) {
return $weight * 10;
}
abstract public function ship($parcel);
}
一開始很合理,但半年後需求來了:
DHL:計費方式不同(體積重)
Uber Eats:即時動態加價
自取:沒有運費
你會開始在 BaseLogistic 裡面看到:
if ($this instanceof DHL) { ... }
if ($this instanceof UberEats) { ... }
這是一個明確的設計退化訊號。
Abstract Class 讓我們在還不了解變化之前,就把邏輯定死了。
單一繼承會快速限制擴充性(PHP 的現實)
在 PHP 中:
Class 只能繼承一個 Abstract Class
但可以實作 多個 Interface
這代表什麼?
如果用了 Abstract Class
class BlackCat extends BaseLogistic
就失去了:
class BlackCat extends BaseLogistic, Trackable, Refundable // 不能
只能選擇:
再塞更多功能進
BaseLogistic或開始用 Trait 補洞(通常會更亂)
相對地,Interface 不會有這個問題
class BlackCat implements LogisticInterface, TrackableInterface, RefundableInterface
IInterface 通常搭配「組合(Composition)」使用,而 Abstract Class 屬於「繼承(Inheritance)」模型。
而現代設計更偏好前者。
Abstract 容易變成「上帝類別」(God Class)
當團隊開始「為了共用而共用」:
「反正大家都會用到,放在 Base 裡面就好。」
Abstract Class 很容易變成這樣:
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 是為了「已經確定不會變的部分」。

