透過類別繼承擴充功能時,父類別逐漸變得肥大的經驗,相信大家都曾遇過。為了加入共用功能,不斷把方法塞進超類別,最後就會面對一個讓人不敢碰的巨大父類別。
協定導向程式設計(POP)正是在這個地方突破OOP的限制。POP不是依據「繼承了什麼」,而是依據「能做什麼」來設計型別。它不採用垂直的繼承結構,而是以協定為單位組合所需能力。
本文將說明POP是為了解決OOP的哪些限制而出現、在實際Swift程式碼中有何不同,以及何時適合或不適合使用。
OOP繼承會在哪裡卡住?
物件導向的繼承確實很強大,但常會在三個地方遇到瓶頸。
第一是單一繼承的限制。Swift類別只能有一個父類別。若要建立同時具備網路與快取能力的型別,只靠繼承無法解決。
第二是肥大基底類別問題。把共用功能集中到父類別後,子類別連自己不用的方法也會全部繼承。
第三是與值型別的相容性。Swift的struct和enum本身不能繼承,但Swift標準函式庫大多使用值型別。
換句話說,只以繼承為中心設計,就很難發揮Swift所推崇的值型別優勢。
什麼是協定導向程式設計(POP)?
2015年Apple在WWDC宣告「Swift是協定導向語言」後,POP便廣為人知。
核心在於可以為協定加入預設實作。使用協定延伸後,不只能定義介面,也能分配實際行為。
例如可以這樣寫。
protocol Greetable {
var name: String { get }
}
extension Greetable {
func greet() -> String {
return "你好, \(name)是"
}
}
struct Person: Greetable {
let name: String
}
print(Person(name: "智勳").greet())
// 輸出:你好,是智勳
只要採用Greetable,就能免費取得greet()。不需繼承,也能把功能分配給struct。
也可以同時採用多個協定。將網路與快取能力分別定義成協定後,就能只把需要的能力附加到所需型別。
OOP繼承與POP有何不同?
將兩者差異整理成表格如下。
| 項目 | OOP繼承 | POP |
|---|---|---|
| 程式碼重複使用方式 | 從父類別繼承 | 透過協定延伸組合 |
| 型別關係 | 垂直(is-a) | 水平(can-do) |
| 多重採用 | 僅能單一繼承 | 同時採用多個協定 |
| 值型別支援 | struct/enum不可用 | struct/enum皆可用 |
| 耦合度 | 父子類別緊密綁定 | 依能力單位鬆散分離 |
簡單說就是這樣。
繼承表示「你是什麼」,協定表示「你能做什麼」。
何時該用POP,何時該避免?
POP並非萬能,應依情境選擇。
| 情境 | 判斷 |
|---|---|
| 想為值型別(struct/enum)分配共用功能 | POP是正解 |
| 需要組合不同能力 | POP較有利 |
| 已有清楚的階層(動物-哺乳類-狗) | 繼承也很適合 |
| 參考共享是核心的物件(例如:視圖控制器) | 類別繼承較自然 |
| 協定切得太細而難以追蹤 | 過度抽象化,應重新檢視 |
選擇標準很簡單:需要值型別與能力組合時用POP,需要清楚階層與參考共享時用繼承。兩者不是對立,而是可依情境並用的工具。
面試時可以這樣問
Q. POP解決了OOP的哪些限制?
它解決單一繼承與肥大基底類別問題。因為可以組合多個協定,只附加所需能力,也能透過協定延伸為無法繼承的struct/enum等值型別提供預設實作。
Q. 協定延伸與類別繼承有何不同?
繼承是垂直的is-a關係,只能有一個父類別;協定是水平的can-do關係,可同時採用多個協定。此外,繼承只能用於參考型別的類別,而協定也適用於值型別。
這不是繼承不好、協定就好的二分法。不過使用Swift開發時,不妨先想「這能不能用協定拆分」,而不是「先用繼承」。這個習慣會帶來更靈活的程式碼。

