淺析面向協議程式設計

zhihaozhang發表於2018-05-21

從程式設計正規化講起

或許你經常聽到諸如物件導向程式設計、程式導向程式設計、面向協議程式設計、函數語言程式設計這些詞,心中也不免疑惑,這些詞都是些啥?

程式設計正規化 on wiki

相比於本文要介紹的面向協議程式設計,物件導向程式設計的名聲似乎更響。物件導向由於C++java的流行,成為近二十年來最為流行的**程式設計正規化。其他程式設計正規化還有比物件導向更早的程式導向程式設計**、指令程式設計,以及新興的以Haskell為代表的**函式式程式設計和蘋果提出的面向協議*程式設計。

程式設計正規化是一類典型的程式設計風格,是指從事軟體工程的一類典型的風格(可以對照方法學)。程式設計範型提供了(同時決定了)程式設計師對程式執行的看法。 ——wiki

我理解的程式設計正規化

說說我的理解,我認為程式設計正規化**反應了某種程式語言的設計者希望程式設計師在使用他設計的語言時,用什麼樣的方式去思考問題。**因此不同的程式設計正規化各有自身的長處,也難免有不足之處,在應對各種問題的時候,某種程式設計正規化可能會更適合一些,選擇得當,程式設計師的工作量會減少很多。

一種語言,可能支援多種程式設計正規化,例如Swift,既支援面向協議程式設計,又支援物件導向程式設計、面向協議程式設計和函數語言程式設計。只是在解決不同任務時,某些正規化更合適。

Swift & 物件導向程式設計

舉個例子,由於歷史原因,在開發Cocoa和cocoa touch程式時,還是廣泛使用了UIkit、Foundation框架和控制器檢視(ViewController),他們使用的是OC時代的物件導向程式設計的方式,從繼承的角度去考慮問題的。

淺析面向協議程式設計

UIkit框架類組織架構圖

Swift & 程式導向程式設計

在解決動畫渲染資料視覺化等問題時,還是以古老的程式導向式的思維方式進行程式設計。因為在畫圖時,先繪製的東西會被後繪製的東西蓋住,形成**層(Layer)**的概念。如果不能保證程式碼是同步一條條執行,那很可能每次渲染出來的圖形是不同的。

Swift & 函數語言程式設計

函數語言程式設計本身是一個很大的課題,精髓是避免使用程式狀態和可變物件,從而降低程式複雜度。函數語言程式設計強調執行的結果,而非執行的過程。我們先構建一系列簡單卻具有一定功能的小函式,然後再將這些函式進行組裝以實現完整的邏輯和複雜的運算,這是函數語言程式設計的基本思想。

Swift在函數語言程式設計方面表現雖不如Haskell來的純粹,但是作為一個比Haskell流行的多的語言,和很多已有的函數語言程式設計語言,Swift在語法上更加優雅靈活,語言本身也遵循了函式式的設計模式,是大部分程式設計師接觸函數語言程式設計的第一門語言。以後有機會單獨介紹一下函數語言程式設計,有興趣的讀者也可以先看chase Zhang的這篇blog

淺析面向協議程式設計

面向協議程式設計出現的歷史

回到今天的主角,面向協議程式設計。面向協議程式設計是由物件導向程式設計演變來的,協議在諸如c++和java等物件導向的語言中有一個別名,介面。(別罵街、別關網頁,好戲在後頭)

物件導向程式設計的好處

物件導向程式設計這些年能夠風光無限,主要是由於它有的很多優點,例如:

  1. 資料封裝
  2. 訪問控制
  3. 型別抽象為類
  4. 繼承關係,更符合人類思維
  5. 程式碼以邏輯關係組織到一起,方便閱讀
  6. 由於繼承、多型的特性,自然設計出高內聚、低耦合的系統結構,使得系統更靈活、更容易擴充套件,而且成本較低
  7. 在設計時,可重用現有的,在以前的專案的領域中已被測試過的類使系統滿足業務需求並具有較高的質量

這麼多優點,不可能一下全拋棄,所以也註定了面向協議程式設計不是一種革命性的程式設計正規化,而是對物件導向程式設計的改良和演變。

Who is Crusty at Apple

長久以來,大家似乎預設了物件導向程式設計的好處都是由class帶來的,但在蘋果公司內部,有個叫Crusty的老兄think different了,他認為這一切是抽象型別帶來的,而不是class帶來的。我們知道,在物件導向程式設計中,介面和類都是對資料型別的抽取,類只是抽象型別眾多實現的手段之一。在很多程式語言中,Struct和列舉同樣可以做到對資料型別的抽取。

It's Type, not Classes. ————Crusty

淺析面向協議程式設計

Crusty認為,較好的抽象型別應該:

  1. 更多地支援值型別,同時也支援引用型別
  2. 更多地支援靜態型別關聯(編譯期),同時也支援動態派發(runtime)
  3. 結構不龐大不復雜
  4. 模型可擴充套件
  5. 不給模型強制新增資料
  6. 不給模型增加初始化任務的負擔
  7. 清楚哪些方法該實現哪些方法不需實現

經過改良的介面,達到了上面的要求,並改名為協議。至此,蘋果決定將物件導向中的繼承父類發展為服從協議,面向協議程式設計出現了。

面向協議程式設計的好處

前面提到了,經過Apple改良的介面達到了上節提出的較好抽象型別的目標。Swift的面向協議程式設計相比於OC的物件導向程式設計的好處主要體現在兩點:1.動態派發的安全性、2.橫切關注點

動態派發的安全性

OC有強大的Runtime,在OC中,message與方法是在執行階段繫結的,而不是編譯階段。簡單的說 [a someFunc] 這樣一個呼叫,在編譯階段,編譯器並不知道someFunc要執行哪段程式碼。這個時候[a someFunc]會被轉換為 objc_msgSend(a, "someFunc"),字面的意思也很容易理解,就是給a這個instance,發“someFunc”這個訊息,以selector的形式。在執行階段,執行到上述的objc_msgSend這個函式時。函式內部會到a對應的記憶體地址,尋找someFunc這個方法的地址,並執行。如果找不到,就會拋一個“unknown selector sent to instance”的異常。(比如.h中宣告瞭方法,但.m中沒有實現,就可以重現這個錯誤)

下面舉的例子來自於喵神在MDCC 16上的演講《面向協議程式設計與 Cocoa 的邂逅》中的ppt:


ViewController *v1 = ...
[v1 myMethod];
AnotherViewController *v2 = ...
[v2 myMethod];
NSObject *v3 = [NSObject new] // v3 ���� `myMethod`
NSArray *array = @[v1, v2, v3];
for (id obj in array) {
    [obj myMethod];
}
// Runtime error:
// unrecognized selector sent to instance blabla

複製程式碼

上面的程式碼是可以編譯過的,但是在執行時程式會崩潰。有了協議(protocol),可以申明陣列的每個物件都是遵從某個協議的,如果塞進去了不遵從該協議的物件,就會報錯,通不過編譯。


protocol Greetable {
    var name: String { get }
    func greet()
}
struct Cat: Greetable {
    let name: String
    func greet() {
        print("meow~ \(name)")
} }

let array: [Greetable] = [
        Person(name: "Wei Wang"),
        Cat(name: "onevcat")]
for obj in array {
    obj.greet()
}

struct Bug: Greetable {
    let name: String
}
// Compiler Error:
// 'Bug' does not conform to protocol 'Greetable'
// protocol requires function 'greet()'

複製程式碼

橫切關注點(Cross-Cutting Concerns)

由於大部分物件導向的程式語言都是單繼承的,導致了某些功能是不同類之間都需要的,但是由於改類已經繼承了其他類,或者無法將不同類之間抽取出更多共性(或者說對於某一個小功能點來這樣做代價太大),成為他們的父類,這樣不得不在每個類裡面重複一遍程式碼,使得程式碼很冗長。這樣的小功能就可以稱為橫切關注點。

還是直接拿喵神的例子,假設我們有一個 ViewController,它繼承自UIViewController,我們向其中新增一個 myMethod,如果這時候我們又有一個繼承自 UITableViewController 的 AnotherViewController,我們也想向其中新增同樣的 myMethod,這時,我們迎來了 OOP 的一大困境,那就是我們很難在不同繼承關係的類裡共用程式碼。這裡的問題用“行話”來說叫做“橫切關注點” (Cross-Cutting Concerns)。我們的關注點 myMethod 位於兩條繼承鏈 (UIViewController -> ViewCotroller 和 UIViewController -> UITableViewController -> AnotherViewController) 的橫切面上。物件導向是一種不錯的抽象方式,但是肯定不是最好的方式。它無法描述兩個不同事物具有某個相同特性這一點。在這裡,特性的組合要比繼承更貼切事物的本質。

淺析面向協議程式設計

在swift中很容易抽取出協議,並用extension關鍵字提供協議的預設實現,從而避免的程式碼的重複。

參考

1.《面向協議程式設計與 Cocoa 的邂逅》 2.wiki:程式設計正規化 3.A Brief Intro to Functional Programming 4.Objective-C 的訊息機制如何理解

相關文章