最近有時間,挑了幾個今年WWDC中比較感興趣的Session視頻來學習,今天就抽時間整理一下關于Swift 2.0
中一個比較新的概念面向協議編程
。
相關的Session視頻鏈接如下:
- Session 408: Protocol-Oriented Programming in Swift
- Session 414: Building Better Apps with Value Types in Swift
寫在前面
面向協議編程是什么?
你可能聽過類似的概念:面向對象編程、函數式編程、泛型編程,再加上蘋果今年新提出的面向協議編程,這些統統可以理解為是一種編程范式。所謂編程范式,是隱藏在編程語言背后的思想,代表著語言的作者想要用怎樣的方式去解決怎樣的問題。不同的編程范式反應在現實世界中,就是不同的編程語言適用于不同的領域和環境,比如在面向對象編程思想中,開發者用對象來描述萬事萬物并試圖用對象來解決所有可能的問題。編程范式都有其各自的偏好和使用限制,所以越來越多的現代編程語言開始支持多范式,使語言自身更強壯也更具適用性。
更多編程范式和相關概念請參看:維基百科:編程范式
對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. 這篇文章的寫作過程持續了很長時間,中間幾乎夭折,最后還是盡量將它寫完整(其實后半部分寫的很水)。面向協議編程是一個比較新的概念,目前只是隱約可以看出它的一些長處(在一些使用面向對象編程并不太適合的地方),不過蘋果已經在自身框架中開始使用了,并確實改善了系統一些類型和方法的使用。
參考資料
- Protocol-Oriented Programming in Swift
- Protocol Oriented Programming
- Protocol-Oriented Programming is Object-Oriented Programming
- Heterogeneous vs Homogeneous Containers in Swift
- If You're Subclassing, You're Doing It Wrong
- 推薦必讀文章:多范式編程語言-以 Swift 為例
最后,讓我們記住這張圖:(Quiz: Who is Crusty at Apple?)