軟體設計

協定導向程式設計(POP),超越OOP限制的方法

協定導向程式設計(POP)不是依據型別繼承了什麼,而是依據型別能做什麼來設計。本文以Swift範例整理繼承的限制、透過協定組合解決的方法,以及應避免使用的情境。

閱讀 3 分鐘
協定導向程式設計(POP),超越OOP限制的方法 封面圖

透過類別繼承擴充功能時,父類別逐漸變得肥大的經驗,相信大家都曾遇過。為了加入共用功能,不斷把方法塞進超類別,最後就會面對一個讓人不敢碰的巨大父類別。

協定導向程式設計(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。

也可以同時採用多個協定。將網路與快取能力分別定義成協定後,就能只把需要的能力附加到所需型別。

讓Person型別採用Greetable・Cacheable協定的類別圖
一個協定負責一項能力,只挑選需要的功能來附加

OOP繼承與POP有何不同?

將兩者差異整理成表格如下。

項目 OOP繼承 POP
程式碼重複使用方式 從父類別繼承 透過協定延伸組合
型別關係 垂直(is-a) 水平(can-do)
多重採用 僅能單一繼承 同時採用多個協定
值型別支援 struct/enum不可用 struct/enum皆可用
耦合度 父子類別緊密綁定 依能力單位鬆散分離

簡單說就是這樣。

繼承表示「你是什麼」,協定表示「你能做什麼」。

顯示OOP與POP比較表的Xcode畫面,以及放著咖啡的桌子
遇到這種情況時,我會先想「能不能用協定拆分」,而不是「先用繼承」

何時該用POP,何時該避免?

POP並非萬能,應依情境選擇。

情境 判斷
想為值型別(struct/enum)分配共用功能 POP是正解
需要組合不同能力 POP較有利
已有清楚的階層(動物-哺乳類-狗) 繼承也很適合
參考共享是核心的物件(例如:視圖控制器) 類別繼承較自然
協定切得太細而難以追蹤 過度抽象化,應重新檢視

選擇標準很簡單:需要值型別與能力組合時用POP,需要清楚階層與參考共享時用繼承。兩者不是對立,而是可依情境並用的工具。

面試時可以這樣問

Q. POP解決了OOP的哪些限制?

它解決單一繼承與肥大基底類別問題。因為可以組合多個協定,只附加所需能力,也能透過協定延伸為無法繼承的struct/enum等值型別提供預設實作。

Q. 協定延伸與類別繼承有何不同?

繼承是垂直的is-a關係,只能有一個父類別;協定是水平的can-do關係,可同時採用多個協定。此外,繼承只能用於參考型別的類別,而協定也適用於值型別。


這不是繼承不好、協定就好的二分法。不過使用Swift開發時,不妨先想「這能不能用協定拆分」,而不是「先用繼承」。這個習慣會帶來更靈活的程式碼。

延伸閱讀