歡迎來到Swift的protocols(協定)和protocols導向的編程教程,在本文中,我們將討論什麼是protocols,以及如何使用它們達到POP(protocol oriented programming:協定導向編程)開發。
我們將首先解釋什麼是protocol,關注protocol和class/structures之間的關鍵差異。接下來,我們將透過範例比較使用協定和類別繼承的差異,展示每種方法的優缺點。之後,我們將討論抽象化(abstraction)和多型(polymorphism),這些物件導向和協定導向編程中的重要概念。然後討論協定擴展(protocol extensions),這是Swift的一個特性,允許開發者替protocols提供預設值和擴增實作,最後,探討如何將OOP搭配POP來解決編程中的各種問題。
什麼是Protocol?
如果讀者對protocols還是相當陌生,你的第一個問題最可能會想問protocol到底是什麼?
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.
因此,根據Swift語言的創建者所示,protocols是定義一組其他類型所需功能的好方法。當我想到協定時,我認為協定提供類型可以做的資訊,Classes和structs則提供物件的資訊,協定則提供物件將會執行的動作。
例如,你可能有一個名為str
的變量,其類型為String
。身為一個開發人員,你應該知道str
代表String
,如果我們定義了一個名為StringProtocol
的協定,它具有所有的String
的API,我們可以擴展任何類型去遵循StringProtocol
(意思是滿足其所有要求),如此一來,即可以使用該對象,讓它就像是一個String
,儘管我們不知道它是什麼!如果看起來像一隻鴨子,游泳像一隻鴨子,叫聲像一隻鴨子,那就是一隻鴨子。我們新的StringProtocol
可以告訴那些遵守它協定的類型能夠做什麼,且不需要知道這些類型的資訊。
協定是抽象的,可以使用協定來替你的程式碼抽象化,現在你知道什麼協定是什麼,以及它們在做什麼,讓我們看看如何在實作中使用它們。
Protocols vs. Subclassing
物件導向編程中最常見的設計模式之一稱為類別繼承(Subclassing)。它允許你在類別之間定義父/子關係:
class MyClass { }
class MySubclass: MyClass { }
在上述關係中,MySubclass
是MyClass
的子類。MySubclass
將自動繼承所有的MyClass
功能,包括屬性、函數、初始化函式等,這意味著所有的MyClass
的成員將自動讓MySubclass
使用:
class MyClass {
var myProperty: String {
return "property"
}
func myFunction() -> String {
return "function"
}
init(string: String) {
print("Initializing an instance of MyClass with \(string)!")
}
}
class MySubclass: MyClass() { }
let s = MySubclass(string: "Hello world") //prints "Initializing an instance of MyClass with hello, world!"
print(s.myProperty) //prints "property"
print(s.myFunction()) //prints "function"
子類也可以覆寫override它們父類的函式,代表他們可以用自己的實作代替:
class MySubclass: MyClass() {
override var myProperty: String {
return "overriden property"
}
override func myFunction() -> String {
return "overriden function"
}
override init(string: String) {
print("Initializing an instance of MySubclass with \(string)!")
}
}
let s = MySubclass(string: "Hello world") //prints "Initializing an instance of MySubclass with hello, world!"
print(s.myProperty) //prints "overriden property"
print(s.myFunction()) //prints "overridden function"
正如你所看到的,這是一個非常強大的設計模式。它允許開發人員在class之間創建緊密的關係。但是,儘管類別繼承很實用,卻仍無法解決我們在構建應用程式時遇到的每個問題。請看下列範例:
class Animal {
func makeSound() { }
}
我們剛剛定義了一個Animal
的類別,在我們的應用程式中代表不同種類的動物。Cool!我們這邊有一個makeSound()
函式,來表示我們的動物擁有的技能。但是有一個問題,動物的聲音都不一樣,那我們可以在makeSound()
功能中做什麼?在實作上,我們可能會這樣做:
class Animal {
func makeSound() { fatalError("Implement me!") }
}
它為Animal
中定義的函數添加了一個fatalError
,讓Animal
成為一個抽象基類,抽象基類只能通過子類實例化。現在,我們可以繼承Animal
並定義我們自己的動物:
class Dog: Animal {
override func makeSound() { print("Woof!") }
}
很好!現在我們有一個Dog
的類別,接著我們可以這樣做:
let rex = Dog()
rex.makeSound() //prints "Woof!"
如果我們忘記覆寫makeSound()
會發生什麼事? 或者如果我們嘗試直接實例化一個Animal
?讓我們來實驗看看:
let tim = Animal()
tim.makeSound() //CRASH
class Cat: Animal { }
let ginger = Cat()
ginger.makeSound() //CRASH
所以,這是一個使用子類化的時候不太理想的情況,我們很常看到這樣的情況。例如,看一下UITableViewDataSource
和UITableViewDelegate
。我們不能使用類別繼承,因為沒有很好的方法來定義tableview的delegate/data的預設行為。實際上,只要沒有在superclass中預設實作行為,子類繼承就會出錯,讓我們回顧一下我們的animal範例,但是改成使用協定:
protocol Sound {
func makeSound()
}
很好,我們定義了一個名為Sound
的protocol,它指定使用者必須有一個makeSound()
函式,我們只關心它是否符合我們對Sound
協定的要求,哪個物件使用它並不重要,讓我們看看這在實踐中如何運作:
struct Dog: Sound {
func makeSound() {
print("Woof")
}
}
struct Tree: Sound {
func makeSound() {
print("Susurrate")
}
}
struct iPhone: Sound {
func makeSound() {
print("Ring")
}
}
這裡Sound
是一個協定,我們可以擴展任何類型以符合Sound
這個protocol,就像我們在上面的例子中所做的那樣。雖然狗是動物,但樹木和iPhone並不是,如果我們從Animal
中將它們進行子類化,其實並不是那麼合理。然而,在使用Sound
協定的情況下,這並不重要,對於希望能發出聲音的對象,唯一要求是採用協定並實現所需的方法。
抽象化與協定擴展
我們學到在某些情況下protocol如何替代子類,但是我們來探討另一種用法:抽象化!我們知道協定允許我們定義其他類型可以遵循的預設功能,那讓我們看看,如果使用這種能力來抽像化類型資訊會發生什麼。其實,我們已經看到了一個例子,通過使用Sound
協定,替樹木和iPhone添加了像動物一樣的能力!
但是,我們是否可將一些邏輯上相關的類型,卻不是一個繼承共同的父類別,通過單一共通接口使用它們呢?這邊如果讀者不太了解,可以想像一下Swift中的各種數字類型,我們有Double
、Float
、Int
及其各種類型(Int8
、Int16
等)和UInt
及其各種類型,你在算術運算過程中是否嘗試過結合它們? 例如,你有沒有嘗試將一個Int
和Float
相除,或者將一個Double
和UInt
相除?請看看下面這段代碼,它在Swift中無法編譯:
let x: Float = 1.2345
let y: Double = 1.3579
let q = x / y
雖然Swift標準函式庫將各種數值類型定義為各自獨立的類型,但對於我們來說,所有數值類型都適用同一個邏輯:數字。
Float
、Double
、Int
和UInt
對於我們來說都是數字,我們能否提供一個協定給Swift中的各種數值類型採用,通過一個共同接口Number
來使用它們,來看看這是不是可以採取的方法,我們首先定義一個名為Number
的協定:
protocol Number {
var floatValue: Float { get } // the { get } means that the variable must be read only
}
我們這個新的protocol有一個遵循條件:floatValue
。從它的宣告可以看到,floatValue
是一個變量,它接受其底層類型並將其轉換為Float
。因此,我們已經定義了一個Number
協定,其中包含一個floatValue
宣告要求,這就意味著,任何遵循floatValue
的有效實作,我們就當它是一個數字。Cool!現在,我們如何將此協定應用於Swift中的既有類型?
答案就是使用extension,Swift的Extension允許我們擴展類型,它可能是原本我們自己定義的,也可能是既有原生的類型,讓我們來看看:
extension Float: Number {
var floatValue: Float {
return self
}
}
extension Double: Number {
var floatValue: Float {
return Double(self)
}
}
//repeat for Int and UInt
感謝我們的extensions,每個Double
、Float
、Int
和UInt
在我們的應用程序中,現在也都是一個Number
。 我們現在可以這樣使用它們:
let x: Double = 1.2345
let f = x.floatValue
很酷,對吧?讓我們做最後一件事來完成我們的Number
類型:定義Number
的特定運算符,它接受Number
的實例(instances)做為參數,並返回Float
!
//MARK: operator definitions
public func +(lhs: Number, rhs: Number) -> Float {
return lhs.floatValue + rhs.floatValue
}
public func -(lhs: Number, rhs: Number) -> Float {
return lhs.floatValue - rhs.floatValue
}
//repeat for * and /
由於我們巧妙地使用protocols和extensions,我們現在可以混用Swift中的數字類型,使用它們來執行算術運算:
let x: Double = 1.2345
ley y: Int = 5
let q = x / y //compiles properly
我們剛剛學到了一個很棒的方式來使用協定:抽象和擴展。通過擴展功能,我們可以定義protocols,然後修改現有的Swift標準庫類型去遵循這個協定,使它們比現有的更強大。
雖然你可能沒有注意到,但是這種方法也啟用了一些名為polymorphism(多型)的東西。多型不是特定於協定導向的編程(多型其實是我們一直與OOP一起使用的東西),它讓我們回到熟悉的物件導向編程原理,在此將其運用在協定導向編程(POP)的環境。在我們的例子中,我們可以使Int
、Float
、Double
和UInt
成為Number
的實例,同時仍保留他們原來的功能!
在OOP中,當類別繼承其他父類別時就會碰上多型,允許它們的物件能能夠身兼父類別與自身類別的特性,POP更進一步,使我們能夠使用幾行代碼即可在整個APP中應用多型原則。使用協定,我們不需要讓Double
、Float
等類別去繼承其他父類別,我們只需要擴展它們,即可為這些類型的每一個實例添加功能,雖然這是一個非常抽象的概念,但它非常有用,在應用程序中應用多型原理的能力將有助於你編寫安全,強大和乾淨的程式碼。
協定擴展與多重繼承
現在,讀者應該已經了解協定的概念,知道它們到底是什麼,以及它們在代碼中提供的好處。很好!但是我們還沒有談論到協定的另外一個重要特點。這是Swift會從物件導向編程語演進到協議導向編程的關鍵,畢竟,我們在Objective C中也有protocols。那麼為什麼Objective C不曾考慮POP,Swift卻開始使用呢?答案在於protocol extension。 就像我們在上一節中看到的那樣,我們可以在Swift中擴展classes和structs以向它們添加功能。然而,通過protocols讓extensions更加強大,因為它們允許你為協定提供預設功能,
這意味著你可以宣告具有自動滿足要求的protocols。
基本上,協定擴展允許開發者保留子類(繼承)的最佳功能之一,同時獲得協定的所有最佳功能。 我們再回到我們的animal範例吧!
想像一下,我們在一個新的世界裡,每一個動物都有自己獨特的聲音(如:woof,meow,moo等)。但我們先設定一個通用的聲音,這裡將預設的通用聲音為”Wow” 現在可以為我們的Sound
協定創建一個extension:
extension Sound {
func makeSound() {
print("Wow")
}
}
現在,若是我們需要宣告一個動物類型可以參考下列格式:
extension MyType: Sound { }
除了添加符合協定要求的宣告外,我們不需要在做其他事,但也可以覆寫預設的功能(就像classes一樣):
extension Snake: Sound {
func makeSound() {
print("hiss")
}
}
所以,這就是使用協定擴展的方法。但等一下,這看起來很像類別繼承,我們正在為我們的功能定義預設值,提供它的預設的實作方法,然後可以選擇性地覆寫它。為什麼我們在這樣的情況下使用協定,而不是子類化?
答案是多重繼承。當你定義一個類別時,它可以有0個或1個父類別,但不能同時拿兩個父類別去定義一個子類別,換句話說,就是無法從兩個父類別繼承功能,但是協定沒有這個限制,物件可以應需求去使用多個所需的協定,繼承所有協定的預設功能。此外,類別可以有選擇地覆寫從協定繼承的功能,就像類別一樣,如下所示:
protocol Sound {
func makeSound()
}
extension Sound {
func makeSound() {
print("Wow")
}
}
protocol Flyable {
func fly()
}
extension Flyable {
func fly() {
print("✈️")
}
}
class Airplane: Flyable { }
class Pigeon: Sound, Flyable { }
class Penguin: Sound { }
let pigeon = Pigeon()
pigeon.fly() // prints ✈️
pigeon.makeSound() // prints Wow
在我們的範例中,我們定義了兩個protocols,分別是Sound
和Flyable
。我們已經知道了什麼是Sound
,但是現在我們知道有能夠使用fly()
的東西是Flyable
協定。然後,我們定義了一個名為Airplane
的類別,假設這架飛機沒有任何聲音,所以Airplane
只繼承Flyable
,並且繼承了它附帶的預設功能。
相反的,企鵝不能飛,所以我們的Penguin
類別採用Sound
協定,但是由於它不能飛,因此,Penguin
不會繼承Flyable
。
上面範例比較有趣的一點在Pigeon(鴿子)
。鴿子
不僅可以發出聲音而且也會飛。我們的Pigeon
類別自動繼承了飛行的能力和發出聲音的能力。如果Sound
和Flyable
被定義為類別,則Pigeon
只能從其中一個繼承功能,而不是兩者。擴展可以說是protocols最有用的功能之一,因為它們允許class和struct能繼承多個其他類型的功能,這是傳統的class/subclass結構無法實現的,無論它的設計如何巧妙。
通過組合protocols和protocol extensions,我們可以使用我們最喜歡的OOP功能(繼承),同時獲得protocols的所有附加優點。協定更安全,更易於使用,並且保持我們的類別結構簡單。此外,使用協定允許我們同時繼承多個parents。很酷,對吧?
Protocols和OOP
我們在本教程中學到了很多關於協定的內容。但是,記住一件很重要的事情,沒有一個編程模式可以解決你在開發上的每一個問題。自從iOS開始以來,我們已經看到物件導向編程、響應式編程、協定導向編程以及無數的其他編程範例。
在應用程式中使用多種編程模式是相當盛行的,只侷限在單一設計模式容易陷入困境,你必須記住,做為一名軟體工程師,你有很多工具可供你使用,只使用一個是不明智的。協定和協定導向編程不是替代OOP的。相反的,它們是用來補充其他編程的不足。當你使用Swift構建下一個應用程序、網站或後端服務時,請記住不要陷入試圖使單一編程方法成為聖杯的陷阱,你必須具有適應性和靈活性,分析每種情況,以確定正確的解決方案。
我希望讀者可以從本教程中獲得很多有價值的知識,如果你喜歡,請不吝與你的朋友和社群分享。謝謝!
FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS
原文:A Beginner’s Guide to Protocols and Protocol Extensions in Swift