【程式設計】TypeScript 實作協定導向程式設計可行嗎?
協定導向程式設計(Protocol Oriented Programming, POP)和物件導向設計(Object Oriented Programming, OOP)其實都是在經歷一個抽象化的過程,將實作的細節先隱藏起來,透過語意來統籌管理規模日漸膨脹,複雜度不斷指數提高的專案內容。因此,由語意上的意圖來理解這兩者的區別可能會更容易一些。
其實這是型態(Shape)與類型(Type)的區別
自從 Swift 語言成為 iOS/macOS 生態系內主流開發語言之一後,協定導向程式設計(Protocol Oriented Programming, POP)就一直不斷被拿來跟大家耳熟的物件導向設計(Object Oriented Programming, OOP)比較。孰優孰劣這件事情已經被廣泛地討論過了,所以阿柴就不再贅述。倒是一直以來,阿柴覺得無論是 POP 或是 OOP,這兩種設計模式其實並非衝突,彼此互斥的存在。身為開發者可以捫心自問,時日至今有任何人敢說經手過的專案不是同時採用 POP 和 OOP 並行嗎? 事實上,說穿了其實也並不奇怪。因為 POP 定義的主要是物件的型態(Shape),而 OOP 描述的主要是物件的類型(Type)。
所以物件的型態與類型說的是什麼樣的一件事
單就詞義的本身,型態與類型確實是很近似的兩個概念。所以在更深入的說明之前,還是來先定義一下這兩個字詞意思。
- 型態:事物的面貌,也就是事物所能表現的動靜態形式。
- 類型:事物的類別,也就是有著共同特性的事物,可以歸類為同一類型。
所以回到物件模型設計時,POP 和 OOP 其實都是在經歷一個抽象化的過程。將實作的細節先隱藏起來,透過語意來統籌管理規模日漸膨脹,複雜度不斷指數提高的專案內容。因此,由語意上的意圖來理解這兩者的區別可能會更容易一些。 POP 處理的是偏向“有嘴巴能夠發出聲音”,“有眼睛能夠看東西”,或是“有腦袋能夠思考”與此相當層面的語意規格。而 OOP 是在前述語意層面聚合後,進行歸納上的分類。例如將“有嘴巴能夠發出聲音”,“有眼睛能夠看東西”,和“有腦袋能夠思考”三個形態組合在一起,歸納得出靈長類這個類型。
介面(Interfaces)和抽象類別(Abstract Classes)正好是型態與類型關係的實作
語意說得再多,終究是屬於抽象層面的思考。將概念實際在程式碼體現時,到底是怎麼樣的一個結果。我們可以透過 TypeScript 的介面和抽象類別,試著仿效 Swift 的 CustomStringConvertible Protocol,實際來感受實作 POP + OOP 設計模式。
介面定義物件的型態
首先,透過關鍵字 interface 來定義介面,並且命名為 ICustomStringConvertible
和 ICustomDebugStringPrintable
。相當於 Swift 的協定宣吿(Protocol Declaration)。
interface ICustomStringConvertible {
description: string
}
interface ICustomDebugStringConvertible {
debugDescription: string
}
上述的宣告,主要是告知了編譯器將會有兩個新增的介面。透過類型檢查確保支持該介面的類別,必然分別會擁有 description 和 debugDescription 兩個屬性。在這個介面約定的基礎上,我們可以像這樣設計 print() 功能,進而輸出該有的類別描述。
function print(arg: ICustomStringConvertible): void {
console.log(arg.description);
}
function debugPrint(arg: ICustomDebugStringConvertible): void {
console.log(arg.debugDescription);
}
抽象類別決定物件的類型
接著下一步,設計一個命名為 AnyObject 的抽象類別,並且支持 ICustomStringConvertible
和 ICustomDebugStringConvertible
介面。這樣一來,意謂著任何繼承 AnyObject 的類別,都必然會有,而且同時具備實作細節的 description 和 debugDescription 屬性。
abstract class AnyObject implements ICustomStringConvertible, ICustomDebugStringConvertible {
abstract description: string;
abstract debugDescription: string;
}
舉例來說,AnObject 類別就是一個繼承 AnyObject 抽象類別的實作例子。
class AnObject extends AnyObject {
description: string = `description for ${ this.constructor.name }`
debugDescription: string = `debugDescription for ${ this.constructor.name }`
}
而實際的執行結果就會如下。也就是基於抽象類別 AnyObject 繼承類別所產生的實例,都能透過 print() 和 debugPrint() 列印出實作細節中所提供的對應內容。
const anObject = new AnObject()
print(anObject)
debugPrint(anObject)
---
[LOG]: "description for AnObject"
[LOG]: "debugDescription for AnObject"
最後的幾點探討
協定導向程式設計或許不是一個理想的修辭方式來描述 TypeScript 的介面。然而無論如何,實際上 TypeScript 和 Swift 確實在抽象化的設計模式上,有些相似而且彼此可以呼應借鑑的地方。所以對於 Swift 程式開發者來說,學習 TypeScript 或許會相對容易上手一些。特別是程式碼的樣式,以及程式碼的構成經驗上,都有些可以互換且對應的地方。更不用說,在個別程式語言上實作抽象化,更是有助於加深對物件導向程式設計的理解。