Protocol Oriented Programming in Swift | Swift面向協議編程初探

最近有時間,挑了幾個今年WWDC中比較感興趣的Session視頻來學習,今天就抽時間整理一下關于Swift 2.0中一個比較新的概念面向協議編程

相關的Session視頻鏈接如下:

寫在前面

面向協議編程是什么?

你可能聽過類似的概念:面向對象編程函數式編程泛型編程,再加上蘋果今年新提出的面向協議編程,這些統統可以理解為是一種編程范式。所謂編程范式,是隱藏在編程語言背后的思想,代表著語言的作者想要用怎樣的方式去解決怎樣的問題。不同的編程范式反應在現實世界中,就是不同的編程語言適用于不同的領域和環境,比如在面向對象編程思想中,開發者用對象來描述萬事萬物并試圖用對象來解決所有可能的問題。編程范式都有其各自的偏好和使用限制,所以越來越多的現代編程語言開始支持多范式,使語言自身更強壯也更具適用性。

更多編程范式和相關概念請參看:維基百科:編程范式

對Swift語言所采用的編程范式感興趣的朋友可以參看這篇文章:多范式編程語言-以 Swift 為例

面向協議編程長什么樣子?

在詳細解釋面向協議編程之前,我們先簡單地概括一下面向協議編程長什么樣子?它與我們熟悉的面向對象編程有什么不一樣?

簡單來說,面向協議編程是在面向對象編程基礎上演變而來,將程序設計過程中遇到的數據類型的抽取(抽象)由使用基類進行抽取改為使用協議(Java語言中的接口)進行抽取。更簡單點舉個栗子來說,一個貓類、一個狗類,我們很容易想到抽取一個描述動物的基類,也會有人想到抽取一個動物通用的協議,那后者就可以被叫做面向協議編程了。什么?就是這樣而已?蘋果官方那么正式的稱Swift是一門支持面向協議編程的語言,難道就是這么簡單的內容?當然不會,有過面向對象編程經驗的人都會清楚,協議的使用限制很多,并不能適用于大多數情況下數據類型的抽象。而在Swift語言中,協議被賦予了更多的功能和更廣闊的使用空間,在Swift 2.0中,更為協議增加了擴展功能,使其能夠勝任絕大多數情況下數據類型的抽象,所以蘋果開始聲稱Swift是一門支持面向協議編程的語言。

面向協議編程對比面向對象編程的好處在哪里?它會對我們程序的設計造成哪些影響?我們會在下文中繼續分析。

寫在中間

離開面向對象我們失去了什么?

首先,讓我們來看看面向對象編程為我們帶來的好處。絕大多數熟悉一種或幾種面向對象編程語言的開發者都能隨口說出幾條面向對象編程的優點,比如數據的封裝、數據訪問的控制、數據類型的抽象、代碼的可讀性和可擴展性等。這意味著離開了面向對象編程我們也就失去了如此多的好處。

哦,天吶!不要這樣好嘛?

回頭仔細想想,這些好處只有面向對象編程才有嘛?蘋果給了我們另一種答案:It's Type, not Classes,是抽象的類型帶給我們如此多的好處,并不是面向對象中的,類只是抽象類型的一種方式。比如在Swift語言中,使用結構體和枚舉也同樣能夠實現對類型的抽象、數據的封裝和訪問控制等,這些好處又都回來了。

那么有沒有什么是類能帶給我們,而結構體和枚舉辦不到的呢?當然有,不然我們真的可以離開面向對象了。面向對象編程還有兩個非常重要的特性我們還沒有提到:繼承和多態。繼承和多態為我們帶來了豐富多彩的世界,想想我們Cocoa Touch中的框架,這才是我們所熟悉的面向對象編程,它使我們能夠輕易地解決所面對的問題,并使我們的代碼具有高度的可定制和可重用性。

我們的世界終于好像正常了。

擁有面向對象我們又得到了什么?

那么,面向對象編程在帶給我們這么多好處的同時,是否還附帶了其他一些特性呢?比如說:要花費的代價。

我們先來看出現的第一個問題,多數面向對象語言中的對象都是使用引用類型,在對象傳遞過程中只是將引用復制一份并指向原有的對象,這樣就會出現問題。比如下面代碼所示的例子:

class Book {
    var name: String
    var pages: Int
    init(name: String, pages: Int) {
        self.name = name
        self.pages = pages
    }
}
class Person {
    var name: String
    var book: Book
    init(name: String, book: Book) {
        self.name = name
        self.book = book
    }
}
let 圍城 = Book(name: "圍城", pages: 888)
let 小明 = Person(name: "小明", book: 圍城) // 小明有一本全新的《圍城》
let 小剛 = Person(name: "小剛", book: 圍城) // 小剛也有一本全新的《圍城》
小明.book.pages = 88 // 小明淘氣把書弄壞了,只剩88頁了
print(小剛.book.pages) // 輸出結果:88  WTF! Where is my new book?

故事的結尾是:小剛因為弄壞書被媽媽打了~ 不對啊,小明哪去了?我也不知道~

相信大多數面向對象編程語言的開發者都明白這是引用傳遞的原因,通常我們的解決辦法也很簡單,每次賦值的時候都先拷貝一份再進行賦值。當我們嘗試在上述代碼中加入copy方法時,卻發現在Swift中對象默認并沒有copy方法,這是因為Swift更推薦使用值類型變量而不是引用類型的變量。如果真的需要調用copy方法,你可以將Book類繼承自NSObject,但這樣的做法真的一點都不優雅,也不夠Swiftpyer。實際上我們的問題也可以采用如下的解決辦法:

class Book {
    var name: String
    var pages: Int
    init(name: String, pages: Int) {
        self.name = name
        self.pages = pages
    }
}
class Person {
    var name: String
    var book: Book
    init(name: String, book: Book) {
        self.name = name
        self.book = Book(name: book.name, pages: book.pages)
    }
}
let 圍城 = Book(name: "圍城", pages: 888)
let 小明 = Person(name: "小明", book: 圍城) // 小明有一本全新的《圍城》
let 小剛 = Person(name: "小剛", book: 圍城) // 小剛也有一本全新的《圍城》
小明.book.pages = 88 // 小明淘氣把書弄壞了,只剩88頁了
print(小剛.book.pages) // 輸出結果:888

我們在Person的構造方法中,為book屬性新創建了一本書,從而保證小明和小剛各自擁有自己的書。這個解決辦法可能并不適用于所有引用類型傳遞的情況,那么在Swift中,最好的解決辦法是什么呢?其實答案很簡單,使用值類型而非引用類型。Swift中許多常見的數據類型、字符串、集合類型,以及結構體和枚舉都是值類型而非引用類型,值類型的變量在賦值時會自動進行一次低消耗的值拷貝,對比對象的copy要更加高效而且不存在線程安全問題。所以我們上面這個故事的最好結局是:將Book修改為結構體類型。

struct Book {
    var name: String
    var pages: Int
    init(name: String, pages: Int) {
        self.name = name
        self.pages = pages
    }
}
struct Person {
    var name: String
    var book: Book
    init(name: String, book: Book) {
        self.name = name
        self.book = book
    }
}
let 圍城 = Book(name: "圍城", pages: 888)
var 小明 = Person(name: "小明", book: 圍城) // 小明有一本全新的《圍城》
let 小剛 = Person(name: "小剛", book: 圍城) // 小剛也有一本全新的《圍城》
小明.book.pages = 88 // 小明淘氣把書弄壞了,只剩88頁了
print(小剛.book.pages) // 輸出結果:888

小剛終于得救了~

想了解更多值類型的使用及其相關信息可以參看:Session 414: Building Better Apps with Value Types in Swift

我們剛剛使用一個例子解釋了面向對象編程中使用引用類型可能出現的問題,接下來我們談論另一個非常重要的話題:繼承的代價。這并不是一個新穎的話題,自面向對象編程誕生之日起就飽受爭議,我們經常要忍受著愈加繁雜和龐大的繼承體系來獲得代碼的可重用性,而且隨著繼承層次的增加,代碼的復雜性會加速增長,隨之而來的bug也會越來越難以發現。這時我們可能需要依靠設計模式來找回我們的思路,然而大多數設計模式只能幫助你理順你的代碼結構,卻在同時更加加深了你的代碼的復雜度。

繼承帶給我們的另一個好處就是多態,多態極大地增強了我們代碼的可擴展性。然而就像“能量守恒定律”一樣,多態也帶來了一定的負面影響,那就是類型信息的缺失。形象一點講,就是我們常常會寫出這樣的代碼:subClassObject as! SubClass,向下類型轉換。

那么問題來了:什么是更好的抽象類型?

蘋果官方對這個問題的回答如下:

  • 更多地支持值類型,同時也支持引用類型
  • 更多地支持靜態類型關聯(編譯期),同時也支持動態派發(運行時)
  • 結構不龐大不復雜
  • 模型可擴展
  • 不給模型強制添加數據
  • 不給模型增加初始化任務的負擔
  • 清楚哪些方法該實現哪些方法不需實現

其實答案就是Swift中的面向協議編程,蘋果只是在自賣自夸而已。

面向協議編程

接下來我們就正式進入Swift的面向協議編程的世界。首先我們來對比如下兩段示例代碼,代碼的功能是定義一個更具擴展性的二分查找法。

class Ordered {
    func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}
class Number: Ordered {
    var value: Double = 0
    override func precedes(other: Ordered) -> Bool {
        return self.value < (other as! Number).value
    }
}
func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
    var lo = 0
    var hi = sortedKeys.count
    while hi > lo {
        let mid = lo + (hi - lo) / 2
        if sortedKeys[mid].precedes(k) { lo = mid + 1 }
        else { hi = mid }
    }
    return lo
}
protocol Ordered {
    func precedes(other: Self) -> Bool
}
struct Number: Ordered {
    var value: Double = 0
    func precedes(other: Number) -> Bool {
        return self.value < other.value
    }
}
func binarySearch<T: Ordered>(sortedKeys: [T], forKey k: T) -> Int {
    var lo = 0
    var hi = sortedKeys.count
    while hi > lo {
        let mid = lo + (hi - lo) / 2
        if sortedKeys[mid].precedes(k) { lo = mid + 1 }
        else { hi = mid }
    }
    return lo
}

應該不難看出兩者之間的區別以及孰優孰劣,簡單解釋一下前者的缺點,反過來也就是后者的優點了。

  • OC語言中沒有抽象類這個概念,所有抽象類都是靠文檔注釋標明,這很蛋疼~
  • 其他類型若想使用該二分查找法,必須繼承自Ordered抽象類,在單繼承體系中,該類型將無法再繼承其他類型
  • 方法參數接收的數組中,類型要求不嚴格,可以放入多種不同類型的Ordered子類對象
  • 基于前一點原因,為保證嚴謹性,必須在方法實現內部增加類型判斷,這更加蛋疼~~

基于上面的例子,我們可以稍微感受到面向協議編程在擴展性上的優勢了,這里再提幾個注意點。

  • Swift 2.0新特性之一,將Self用于約束泛型,功能類似于OC中的instancetype,示例:extension Ordered where Self: Comparable
  • Swift 2.0另一個重要的新特性,協議可擴展,意味著你不僅可以擴展一個類型使其遵守Ordered協議,還可以直接擴展某個協議,詳見如下兩段代碼示例。
// 擴展類型
extension Int: Ordered {
    func precedes(other: Int) -> Bool {
        return self < other
    }
}
extension String: Ordered {
    func precedes(other: String) -> Bool {
        return self < other
    }
}
let intIndex = binarySearch([2, 3, 5, 7], forKey: 5) // 輸出結果2
let stringIndex = binarySearch(["2", "3", "5", "7"], forKey: "5") // 輸出結果2
// 擴展協議:方式一
//extension Comparable {
//    func precedes(other: Self) -> Bool {
//        return self < other
//    }
//}
// 擴展協議:方式二(Swift 2.0的推薦方式)
extension Ordered where Self: Comparable {
    func precedes(other: Self) -> Bool {
        return self < other
    }
}
extension Int: Ordered {}
extension String: Ordered {}
let intIndex = binarySearch([2, 3, 5, 7], forKey: 5) // 輸出結果2
let stringIndex = binarySearch(["2", "3", "5", "7"], forKey: "5") // 輸出結果2

從上面的代碼我們可以看出,協議可擴展所帶來的功能之一就是能夠為協議中的方法提供默認實現。

更多協議可擴展所帶來的功能可以參看RayWenderlich上的這篇文章:

關于面向協議編程的完整示例程序可以參看蘋果官方的示例代碼:

寫在最后

個人總結

面向對象編程和面向協議編程最明顯的區別在于程序設計過程中對數據類型的抽取(抽象)上,面向對象編程使用類和繼承的手段,數據類型是引用類型;而面向協議編程使用的是遵守協議的手段,數據類型是值類型(Swift中的結構體或枚舉)。

面向協議編程是在面向對象編程基礎上發展而來的,而并不是完全背離面向對象編程的思想。

面向對象編程是偉大的編程思想,也是當今主流的編程思想,它的問題在于被過多的使用在其實并不需要使用它的情況下。

Swift是一門支持多編程范式的語言,既支持面向對象編程,也支持面向協議編程,同時還支持函數式編程。在項目開發過程中,控制器和視圖部分由于使用系統框架,應更多采用面向對象編程的方式;而模型或業務邏輯等自定義類型部分,則應優先考慮面向協議編程。

PS. 這篇文章的寫作過程持續了很長時間,中間幾乎夭折,最后還是盡量將它寫完整(其實后半部分寫的很水)。面向協議編程是一個比較新的概念,目前只是隱約可以看出它的一些長處(在一些使用面向對象編程并不太適合的地方),不過蘋果已經在自身框架中開始使用了,并確實改善了系統一些類型和方法的使用。

參考資料

最后,讓我們記住這張圖:(Quiz: Who is Crusty at Apple?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容