1. 程式人生 > >面向協議程式設計與 Cocoa 的邂逅(上)

面向協議程式設計與 Cocoa 的邂逅(上)

作者簡介: 王巍(@onevcat),江湖人稱“喵神”,iOS 和 Unity3D 開發者,旅居日本,目前供職於 LINE,著有《Swifter - 100 個 Swift 必備 tips》,同時也是翻譯專案 ObjC 中國的組織者和管理者,維護 VVDocumenter-Xcode 及 Kingfisher 等開源專案,部落格地址:https://onevcat.com
技術之路,我們共同進步,歡迎技術投稿、約稿、給文章糾錯,請傳送郵件至[email protected]

【CSDN編者按】在 MDCC 2016 iOS 開發峰會從策劃伊始到正式舉行與結束,三個多月的時間裡,喵神一直鼎力參與其中,除卻自己準備演講內容之外,還承擔著整場內容稽核建議的工作,他的 Keynote 未刪減版整整 135 頁,並在現場進行了實操演示,在工作十分忙碌之時,喵神終於把文章寫完了,特此聊表感謝,也希望能夠讓你們有所受益。

本文是筆者在 MDCC 16 (移動開發者大會) 上 iOS 專場中的主題演講的文字整理。您可以在這裡找到演講使用的 Keynote,部分示例程式碼可以在 MDCC 2016 的官方 repo 中找到。因為全部內容比較長,所以分成了上下兩個部分,本文 (上) 主要介紹了一些理論方面的內容,包括面向物件程式設計存在的問題,面向協議的基本概念和決策模型等,下半部分主要展示了一些筆者日常使用面向協議思想和 Cocoa 開發結合的示例程式碼,並對其進行了一些解說。

引子

面向協議程式設計 (Protocol Oriented Programming,以下簡稱 POP) 是 Apple 在 2015 年 WWDC 上提出的 Swift 的一種程式設計正規化。相比與傳統的面向物件程式設計 (OOP),POP 顯得更加靈活。結合 Swift 的值語義特性和 Swift 標準庫的實現,這一年來大家發現了很多 POP 的應用場景。本次演講希望能在介紹 POP 思想的基礎上,引入一些日常開發中可以使用 POP 的場景,讓與會來賓能夠開始在日常工作中嘗試 POP,並改善程式碼設計。

起・初識 - 什麼是 Swift 協議

Protocol

Swift 標準庫中有 50 多個複雜不一的協議,幾乎所有的實際型別都是滿足若干協議的。protocol 是 Swift 語言的底座,語言的其他部分正是在這個底座上組織和建立起來的。這和我們熟知的面向物件的構建方式很不一樣。

一個最簡單但是有實際用處的 Swift 協議定義如下:

protocol Greetable {
    var name: String { get }
    func greet()
}

這幾行程式碼定義了一個名為 Greetable 的協議,其中有一個 name 屬性的定義,以及一個 greet 方法的定義。

所謂協議,就是一組屬性和/或方法的定義,而如果某個具體型別想要遵守一個協議,那它需要實現這個協議所定義的所有這些內容。協議實際上做的事情不過是“關於實現的約定”。

面向物件

在深入 Swift 協議的概念之前,我想先重新讓大家回顧一下面向物件。相信我們不論在教科書或者是部落格等各種地方對這個名詞都十分熟悉了。那麼有一個很有意思,但是其實並不是每個程式設計師都想過的問題,面向物件的核心思想究竟是什麼?

我們先來看一段面向物件的程式碼:

class Animal {
    var leg: Int { return 2 }
    func eat() {
        print("eat food.")
    }
    func run() {
        print("run with \(leg) legs")
    }
}

class Tiger: Animal {
    override var leg: Int { return 4 }
    override func eat() {
        print("eat meat.")
    }
}

let tiger = Tiger()
tiger.eat() // "eat meat"
tiger.run() // "run with 4 legs"

父類 Animal 定義了動物的 leg (這裡應該使用虛類,但是 Swift 中沒有這個概念,所以先請無視這裡的 return 2),以及動物的 eatrun 方法,併為它們提供了實現。子類的 Tiger 根據自身情況重寫了 leg (4 條腿)和 eat (吃肉),而對於 run,父類的實現已經滿足需求,因此不必重寫。

我們看到 TigerAnimal 共享了一部分程式碼,這部分程式碼被封裝到了父類中,而除了 Tiger 的其他的子類也能夠使用 Animal 的這些程式碼。這其實就是 OOP 的核心思想 - 使用封裝和繼承,將一系列相關的內容放到一起。我們的前輩們為了能夠對真實世界的物件進行建模,發展出了面向物件程式設計的概念,但是這套理念有一些缺陷。雖然我們努力用這套抽象和繼承的方法進行建模,但是實際的事物往往是一系列特質的組合,而不單單是以一脈相承並逐漸擴充套件的方式構建的。所以最近大家越來越發現面向物件很多時候其實不能很好地對事物進行抽象,我們可能需要尋找另一種更好的方式。

面向物件程式設計的困境

橫切關注點

我們再來看一個例子。這次讓我們遠離動物世界,回到 Cocoa,假設我們有一個 ViewController,它繼承自 UIViewController,我們向其中新增一個 myMethod

class ViewCotroller: UIViewController
{
    // 繼承
    // view, isFirstResponder()...

    // 新加
    func myMethod() {

    }
}

如果這時候我們又有一個繼承自 UITableViewControllerAnotherViewController,我們也想向其中新增同樣的 myMethod

class AnotherViewController: UITableViewController
{
    // 繼承
    // tableView, isFirstResponder()...

    // 新加
    func myMethod() {

    }
}

這時,我們迎來了 OOP 的第一個大困境,那就是我們很難在不同繼承關係的類裡共用程式碼。這裡的問題用“行話”來說叫做“橫切關注點” (Cross-Cutting Concerns)。我們的關注點 myMethod 位於兩條繼承鏈 (UIViewController -> ViewCotrollerUIViewController -> UITableViewController -> AnotherViewController) 的橫切面上。面向物件是一種不錯的抽象方式,但是肯定不是最好的方式。它無法描述兩個不同事物具有某個相同特性這一點。在這裡,特性的組合要比繼承更貼切事物的本質。

想要解決這個問題,我們有幾個方案:

  • Copy & Paste

    這是一個比較糟糕的解決方案,但是演講現場還是有不少朋友選擇了這個方案,特別是在工期很緊,無暇優化的情況下。這誠然可以理解,但是這也是壞程式碼的開頭。我們應該儘量避免這種做法。

  • 引入 BaseViewController

在一個繼承自 UIViewControllerBaseViewController 上新增需要共享的程式碼,或者乾脆在 UIViewController 上新增 extension。看起來這是一個稍微靠譜的做法,但是如果不斷這麼做,會讓所謂的 Base 很快變成垃圾堆。職責不明確,任何東西都能扔進 Base,你完全不知道哪些類走了 Base,而這個“超級類”對程式碼的影響也會不可預估。

  • 依賴注入

通過外界傳入一個帶有 myMethod 的物件,用新的型別來提供這個功能。這是一個稍好的方式,但是引入額外的依賴關係,可能也是我們不太願意看到的。

  • 多繼承

當然,Swift 是不支援多繼承的。不過如果有多繼承的話,我們確實可以從多個父類進行繼承,並將 myMethod 新增到合適的地方。有一些語言選擇了支援多繼承 (比如 C++),但是它會帶來 OOP 中另一個著名的問題:菱形缺陷。

菱形缺陷

上面的例子中,如果我們有多繼承,那麼 ViewControllerAnotherViewController 的關係可能會是這樣的:

在上面這種拓撲結構中,我們只需要在 ViewController 中實現 myMethod,在 AnotherViewController 中也就可以繼承並使用它了。看起來很完美,我們避免了重複。但是多繼承有一個無法迴避的問題,就是兩個父類都實現了同樣的方法時,子類該怎麼辦?我們很難確定應該繼承哪一個父類的方法。因為多繼承的拓撲結構是一個菱形,所以這個問題又被叫做菱形缺陷 (Diamond Problem)。像是 C++ 這樣的語言選擇粗暴地將菱形缺陷的問題交給程式設計師處理,這無疑非常複雜,並且增加了人為錯誤的可能性。而絕大多數現代語言對多繼承這個特性選擇避而遠之。

動態派發安全性

Objective-C 恰如其名,是一門典型的 OOP 語言,同時它繼承了 Small Talk 的訊息傳送機制。這套機制十分靈活,是 OC 的基礎思想,但是有時候相對危險。考慮下面的程式碼:

ViewController *v1 = ...
[v1 myMethod];

AnotherViewController *v2 = ...
[v2 myMethod];

NSArray *array = @[v1, v2];
for (id obj in array) {
    [obj myMethod];
}

我們如果在 ViewControllerAnotherViewController 中都實現了 myMethod 的話,這段程式碼是沒有問題的。myMethod 將會被動態傳送給 array 中的 v1v2。但是,要是我們有一個沒有實現 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

編譯依然可以通過,但是顯然,程式將在執行時崩潰。Objective-C 是不安全的,編譯器預設你知道某個方法確實有實現,這是訊息傳送的靈活性所必須付出的代價。而在 app 開發看來,用可能的崩潰來換取靈活性,顯然這個代價太大了。雖然這不是 OOP 正規化的問題,但它確實在 Objective-C 時代給我們帶來了切膚之痛。

三大困境

我們可以總結一下 OOP 面臨的這幾個問題。

  • 動態派發安全性
  • 橫切關注點
  • 菱形缺陷

首先,在 OC 中動態派發讓我們承擔了在執行時才發現錯誤的風險,這很有可能是發生在上線產品中的錯誤。其次,橫切關注點讓我們難以對物件進行完美的建模,程式碼的重用也會更加糟糕。

承・相知 - 協議擴充套件和麵向協議程式設計

使用協議解決 OOP 困境

協議並不是什麼新東西,也不是 Swift 的發明。在 Java 和 C# 裡,它叫做 Interface。而 Swift 中的 protocol 將這個概念繼承了下來,併發揚光大。讓我們回到一開始定義的那個簡單協議,並嘗試著實現這個協議:

protocol Greetable {
    var name: String { get }
    func greet()
}
struct Person: Greetable {
    let name: String
    func greet() {
        print("你好 \(name)")
    }
}
Person(name: "Wei Wang").greet()

實現很簡單,Person 結構體通過實現 namegreet 來滿足 Greetable。在呼叫時,我們就可以使用 Greetable 中定義的方法了。

動態派發安全性

除了 Person,其他型別也可以實現 Greetable,比如 Cat

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()
}
// 你好 Wei Wang
// meow~ onevcat

對於沒有實現 Greetbale 的型別,編譯器將返回錯誤,因此不存在訊息誤傳送的情況:

struct Bug: Greetable {
    let name: String
}

// Compiler Error: 
// 'Bug' does not conform to protocol 'Greetable'
// protocol requires function 'greet()'

這樣一來,動態派發安全性的問題迎刃而解。如果你保持在 Swift 的世界裡,那這個你的所有程式碼都是安全的。

  • ✅ 動態派發安全性
  • 橫切關注點
  • 菱形缺陷

橫切關注點

使用協議和協議擴充套件,我們可以很好地共享程式碼。回到上一節的 myMethod 方法,我們來看看如何使用協議來搞定它。首先,我們可以定義一個含有 myMethod 的協議:

protocol P {
    func myMethod()
}

注意這個協議沒有提供任何的實現。我們依然需要在實際型別遵守這個協議的時候為它提供具體的實現:

// class ViewController: UIViewController
extension ViewController: P {
    func myMethod() {
        doWork()
    }
}

// class AnotherViewController: UITableViewController
extension AnotherViewController: P {
    func myMethod() {
        doWork()
    }
}

你可能不禁要問,這和 Copy & Paste 的解決方式有何不同?沒錯,答案就是 – 沒有不同。不過稍安勿躁,我們還有其他科技可以解決這個問題,那就是協議擴充套件。協議本身並不是很強大,只是靜態型別語言的編譯器保證,在很多靜態語言中也有類似的概念。那到底是什麼讓 Swift 成為了一門協議優先的語言?真正使協議發生質變,並讓大家如此關注的原因,其實是在 WWDC 2015 和 Swift 2 釋出時,Apple 為協議引入了一個新特性,協議擴充套件,它為 Swift 語言帶來了一次革命性的變化。

所謂協議擴充套件,就是我們可以為一個協議提供預設的實現。對於 P,可以在 extension P 中為 myMethod 新增一個實現:

protocol P {
    func myMethod()
}

extension P {
    func myMethod() {
        doWork()
    }
}

有了這個協議擴充套件後,我們只需要簡單地宣告 ViewControllerAnotherViewController 遵守 P,就可以直接使用 myMethod 的實現了:

extension ViewController: P { }
extension AnotherViewController: P { }

viewController.myMethod()
anotherViewController.myMethod()

不僅如此,除了已經定義過的方法,我們甚至可以在擴充套件中新增協議裡沒有定義過的方法。在這些額外的方法中,我們可以依賴協議定義過的方法進行操作。我們之後會看到更多的例子。總結下來:

  • 協議定義
    提供實現的入口
    遵循協議的型別需要對其進行實現

  • 協議擴充套件
    為入口提供預設實現
    根據入口提供額外實現
    這樣一來,橫切點關注的問題也簡單安全地得到了解決。

  • ✅ 動態派發安全性

  • ✅ 橫切關注點
  • 菱形缺陷

菱形缺陷

最後我們看看多繼承。多繼承中存在的一個重要問題是菱形缺陷,也就是子類無法確定使用哪個父類的方法。在協議的對應方面,這個問題雖然依然存在,但卻是可以唯一安全地確定的。我們來看一個多個協議中出現同名元素的例子:

protocol Nameable {
    var name: String { get }
}

protocol Identifiable {
    var name: String { get }
    var id: Int { get }
}

如果有一個型別,需要同時實現兩個協議的話,它必須提供一個 name 屬性,來同時滿足兩個協議的要求:

struct Person: Nameable, Identifiable {
    let name: String 
    let id: Int
}

// `name` 屬性同時滿足 Nameable 和 Identifiable 的 name

這裡比較有意思,又有點讓人困惑的是,如果我們為其中的某個協議進行了擴充套件,在其中提供了預設的 name 實現,會如何。考慮下面的程式碼:

extension Nameable {
    var name: String { return "default name" }
}

struct Person: Nameable, Identifiable {
    // let name: String 
    let id: Int
}

// Identifiable 也將使用 Nameable extension 中的 name

這樣的編譯是可以通過的,雖然 Person 中沒有定義 name,但是通過 Nameablename (因為它是靜態派發的),Person 依然可以遵守 Identifiable。不過,當 NameableIdentifiable 都有 name 的協議擴充套件的話,就無法編譯了:

extension Nameable {
    var name: String { return "default name" }
}

extension Identifiable {
    var name: String { return "another default name" }
}

struct Person: Nameable, Identifiable {
    // let name: String 
    let id: Int
}

// 無法編譯,name 屬性衝突

這種情況下,Person 無法確定要使用哪個協議擴充套件中 name 的定義。在同時實現兩個含有同名元素的協議,並且它們都提供了預設擴充套件時,我們需要在具體的型別中明確地提供實現。這裡我們將 Person 中的 name 進行實現就可以了:

extension Nameable {
    var name: String { return "default name" }
}

extension Identifiable {
    var name: String { return "another default name" }
}

struct Person: Nameable, Identifiable {
    let name: String 
    let id: Int
}

Person(name: "onevcat", id: 123).name // onevcat

這裡的行為看起來和菱形問題很像,但是有一些本質不同。首先,這個問題出現的前提條件是同名元素以及同時提供了實現,而協議擴充套件對於協議本身來說並不是必須的。其次,我們在具體型別中提供的實現一定是安全和確定的。當然,菱形缺陷沒有被完全解決,Swift 還不能很好地處理多個協議的衝突,這是 Swift 現在的不足。

  • ✅ 動態派發安全性
  • ✅ 橫切關注點
  • ❓菱形缺陷

本文的下半部分將展示一些筆者日常使用面向協議思想和 Cocoa 開發結合的示例程式碼,並對其進行了一些解說。

瞭解最新移動開發相關資訊和技術,請關注 mobilehub 公眾微訊號(ID: mobilehub)。

mobilehub

相關推薦

面向協議程式設計 Cocoa邂逅

作者簡介: 王巍(@onevcat),江湖人稱“喵神”,iOS 和 Unity3D 開發者,旅居日本,目前供職於 LINE,著有《Swifter - 100 個 Swift 必備 tips》,同時也是翻譯專案 ObjC 中國的組織者和管理者,維護 VVDo

從蘇寧電器到卡巴斯基第34篇:我卡巴斯基的邂逅

前傳——我與病毒的故事        當我還在讀小學的時候,我們家就購置了一臺386電腦。當時還是DOS時代,不友好的命令列介面實在讓我不知道這個電腦究竟能幹什麼。後來有懂行的,告訴我電腦裡面有遊戲,於是我就天天玩五子棋、推箱子、TT打字還有《妖魔道

多執行緒高併發程式設計之基礎知識

前言 幾乎所有的程式設計師都知道,現代作業系統進行資源分配的最小單元是程序,而作業系統進行運算排程的最小單元是執行緒,其實,在Linux中執行緒也可以看作是一種輕量級的程序,那麼執行緒是包含於程序之中的,是程序中實際的運作單位;同一程序中的多個執行緒共用同一塊

java面向對象設計模式

工廠方法模式 java 選擇 缺點 一個 聯系 面向 抽象工廠 pan 第五式 抽象工廠模式 定義:提供一個創建一系列相關或相互依賴對象的接口,而無需指定他們具體的類。(創建的對象之間有約束) 抽象工廠模式的本質:選擇產品簇的實現 優點:分離接口和產品簇,使得切換產品簇變得

Apache +Jetty的負載均衡叢集配置

                                         

20172309_《程式設計資料結構》_課堂測試修改報告。

20172309_《程式設計與資料結構(下)》_課堂測試修改報告。 課程:《程式設計與資料結構》 班級:1723 姓名: 王志偉 學號:20172309 實驗教師:王志強老師 實驗日期:2018年6月13日 必修/選修: 必修 實驗內容: 查詢演算法綜合示例: 實驗過程及結果

2018-2019-20172309 《程式設計資料結構》實驗二報告

課程:《程式設計與資料結構(下)》 班級:1723 姓名: 王志偉 學號:20172309 實驗教師:王志強老師 實驗日期:2018年11月2日 必修/選修: 必修 實驗內容: 實驗一:實現二叉樹。 1.參考教材p212,完成鏈樹LinkedBinaryTree的實現(getRight,conta

Spark2.1.0模型設計基本架構

  隨著近十年網際網路的迅猛發展,越來越多的人融入了網際網路——利用搜索引擎查詢詞條或問題;社交圈子從現實搬到了Facebook、Twitter、微信等社交平臺上;女孩子們現在少了逛街,多了在各大電商平臺上的購買;喜歡棋牌的人能夠在對戰平臺上找到世界各地的玩家對弈。在國內隨著網民數量的持續增加,造成網際網路公

linux裡的程序執行緒

在學習linux的過程中,程序與執行緒可謂一對好兄弟,是必然要掌握的內容。 一:何所謂程序 何所謂執行緒         程序:程序(Process)是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配與排程的基本單位。         執行緒:執行緒是作

鄭南寧院士會議演講——直覺性AI無人駕駛

博主最近參加了2018年中國機器人學術年會,覺得鄭院士的報告很有深度,所以先把人工智慧部分整理出來與大家共勉:   人工智慧的四個階段 1960-1980 1980-2000 2000- 未來

從壹開始微服務 [ DDD ] 之五 ║聚合:實體值物件

前言 哈嘍,老張是週四放鬆又開始了,這些天的工作真的是繁重,三個專案同時啟動,沒辦法,只能在深夜寫文章了,現在時間的週四凌晨,白天上班已經沒有時間開始寫文章了,希望看到文章的小夥伴,能給個辛苦贊

面向物件程式設計2018上機題3

/*設計一個類,用於對字串進行管理。要求: (1)在定義物件時,能夠確定字串的最大長度並清空字串。 (2)定義成員函式input, 用於從鍵盤為字串輸入資料。 (3)定義運算子過載函式,用於判斷兩個物件中的字串是否相同。 (4)定義成員函式exchange,用於互換字

Silverlight微軟技術:微軟拋棄Silverlight了麼?

話說,在最近的PDC上,微軟的副總裁Bob Muglia說了類似的話“我們對Silverlight的策略改變了,我們要用HTML5來實現跨平臺的應用程式”,於是乎,社群鋪天蓋地響起了“微軟要拋棄Silverlight”的調調。出現這個說法並不奇怪,媒體轉述一遍,再給社群理解一下,很正常。但是我覺得奇怪甚至有些

遊戲程式設計十年總結

大學之路     高考之後的暑假,決定系統地學一下C++,由於家裡拉上了寬頻,所以極大方便了我找資料,在網上找到了孫鑫老師的C++視訊,通過這套視訊對C++、面向物件和MFC,都有了一個初步的瞭解,但對很多概念都還是一知半解。     暑假很快就過了,2008年的九月份,來到了學校報到,飽受了缺乏資料

Spring Cloud 探索 | 服務註冊發現 Eureka

先在這裡宣告一下:這一系列的文章都是自己查詢網上資料學習而來,不可避免有些內容從其他地方copy過來,如有侵犯請聯絡我刪除,謝謝!(引用部分都會添加註腳) 本系列文章的寫作環境為:Spring Boot 2.0.7.RELEASE、Spring Cloud Fi

Java高併發程式設計 JDK併發包

1. 重入鎖  重入鎖可以完全替代synchronized關鍵字。使用java.util.concurrent.locks.ReentrantLock類實現,下面是一個重入鎖的簡單例子: package cn.net.bysoft.java.concurrency.desi

【STP】生成樹協議及STP 802.1D

通過在交換機上部署STP可以解決二層環路問題。交換機之間需要交換生成樹的協議訊息來檢測橋接環路,以保證STP工作的正常,該訊息稱為網橋協議資料單元(BPDU,Bridge Protocol Data Unit), BPDU訊息中包含著用於STP選舉的各項引數。STP工作的結果是經過一系列的”選舉“後將某個或

淺談面向物件程式設計和麵向過程

       今天小編被老師點名叫起來回答“來說說面向物件是什麼”,“不知道”,“那面向過程呢?”,“不知道”,“這麼直接?你下次好好聽聽,明天接著問你。”嘛,今天已經快過去了,那我們就趁著晚自習的時候,好好來總結一下,什麼面向物件,什麼是面向過程,為什麼會有面向物件這個東

Unity3D案例太空射擊Space Shooter流程介紹程式碼分析

最近開始接觸遊戲製作,用Unity製作一些簡單的遊戲進行入門。這幾篇部落格總結了Space Shooter的製作流程,並對程式碼進行了分析。一是方便自己日後進行回顧與補充,二是為了讓讓更多的遊戲愛好者接觸遊戲開發,少走彎路。 Space Shooter是一個入門級的專案,很

UML面向物件建模設計——筆記

UML面向物件建模與設計(第二版)筆記——第三部分:實現 實現 1微調類 2微調泛化 3實現關聯 測試 1.實現 實現階段是對前面設計的結果用具體的語言表示的結果。在寫程式碼的時候我們會增加一些細節。首先我們應該解決超出語言層