第一章.面向對象與面向協(xié)議編程
本書是關于面向協(xié)議編程。當蘋果2015年的開發(fā)者大會上發(fā)布了Swift2,他們也宣布Swift是第一種面向協(xié)議編程的語言。通過它的名字,我們可能會以為面向協(xié)議編程都是關于協(xié)議。并不是這樣。這是一個錯誤的猜想。這是一個不僅是關于寫應用,更是一個思考編程的方法。
在這章中,你將會學習:
- Swift作為面向對象編程語言該如何使用
- Swift作為面向協(xié)議編程語言該如何使用
- 面向對象編程與面向協(xié)議編程的區(qū)別
- 相比于面向對象編程,面向協(xié)議編程所提供的優(yōu)勢
本書是關于面向協(xié)議編程,我們將會從討論Swift如何被作為面向對象編程語言來開始。理解面向對象編程將會幫助我們理解面向協(xié)議編程,并且洞悉一些面向協(xié)議編程被設計用來解決的一些問題。
Swift是一門面向對象編程的語言
面向對象編程是一種設計哲學。使用面向對象編程的語言而不是面向過程的語言(比如C和Pascal)來寫APP是完全不同的。面向過程的語言通過依賴程序一步步的告訴電腦怎么做。這可能看起來像一個給了明顯名字的聲明。但是最基本的,當我偶們考慮面向對象編程的時候,我們需要考慮到對象
該對象是一種數(shù)據結構,其以屬性的形式包含關于對象的屬性的信息,并且以方法的形式由對象執(zhí)行或對對象執(zhí)行的操作。對象可以被考慮成一種東西,在英語中,他們很正常的被考慮成介詞。這些對象可以是真是世界或者虛擬對象。如果你環(huán)視四周,你將會看到很多真實世界的對象,并且在那里,它們都可以使用屬性與操作以面向對象的方式來建模。
當我哦在寫這章的時候,我看著窗外,并且看到一個湖,很多樹,草地,我的狗和我院子里的柵欄。所有這些東西都可以使用屬性與操作來建模成一個對象。
當我寫這篇文章的時候,我也在思考一種我一直喜歡的運動飲料。這種能量飲料叫:Jolt。我不確定還有多少人記得Jolt蘇打后者Jolt能量飲料,但是沒有它們,我甚至都不能從學院畢業(yè)。一罐Jolt可以建模成為一個帶有屬性(凈含量,咖啡因含量,溫度和大小)和操作(喝和溫度改變)。
我們可以把一罐Jolt放到一個Cooler的地方來給它降溫。這個Cooler也可以被建模成一個對象,因為它有屬性(溫度,一罐Jolt,可以放的最大罐數(shù))和操作(添加和移走Jolt)。
對象使得面向對象編程如此強大。使用對象,我們可以對真實世界的對象建模,比如一罐Jolt,我們也可以對虛擬世界里的對象建模,比如在電子游戲里的角色。這些對象可以在我們的應用里互動來構建真實世界的行為,或者在我們的虛擬世界里我們想要的行為。
在一個電腦應用內,我們不能在沒有藍圖的情況下創(chuàng)建一個對象,這個藍圖告訴應用這個對象將會有什么屬性和操作。在大多數(shù)的面向對象語言,這個藍圖以類的形式出現(xiàn)。一個類被構造用以允許我們來把對象的屬性和操作封裝成單一類型,該類型對我們試圖代表的對象進行建模.
我們在我們的類中使用初始化器來創(chuàng)建類的實例。我們一般使用這些初始化器來為我們的對象初始化一類屬性的值,或者執(zhí)行其他類需要的初始化工作。一旦我們創(chuàng)建了一個類的實例,我們可以在我們的代理里使用它。
所有這些面向對象編程的解釋都是好的,但是沒有比真實的代碼更好的演示觀念的方法了。讓我們看看我們怎么能夠使用Swift中的類來建模一罐Jolt和Cooler來對Jolk降溫。我們將會從一罐Jolt的建模開始,例子:
class Jolt {
var volume: Double
var caffeine: Double
var temperature: Double
var canSize: Double
var description: String
init(volume: Double, caffeine: Double, temperature: Double) {
self.volume = volume
self.caffeine = caffeine
self.temperature = temperature
self.description = "Jolt energy drink"
self.canSize = 24
}
func drinking(amount: Double) {
volume -= amount
}
func temperatureChange(change: Double) {
temperature += change
}
}
在這個Jolt類里,我們定義了5個屬性:volume(凈含量),caffeine(咖啡因含量),temperature(當前罐里的溫度),description(產品說明)和cansize(罐頭本身的大小)。之后我們定義了一個初始化器,當我們創(chuàng)建類實例的時候,初始化器將會對對象的屬性做初始化。最后,我們?yōu)楣拮佣x了兩個操作。這兩個操作是drinking(某人喝的時候會調用)。和temperatureChange(罐身溫度改變的時候會調用)。
現(xiàn)在,讓我們看看我們怎么對一個Cooler建模,來讓這個Cooler來給我們Jolt降溫。畢竟,沒人喜歡熱的Jolt:
class Cooler {
var temperature: Double
var cansOfJolt = [Jolt]()
var maxCans: Int
init(temperature: Double, maxCans: Int) {
self.temperature = temperature
self.maxCans = maxCans
}
func addJolt(jolt: Jolt) -> Bool {
if cansOfJolt.count < maxCans {
cansOfJolt.append(jolt)
return true
}else{
return false
}
}
func removeJolt() -> Jolt? {
if cansOfJolt.count > 0 {
return cansOfJolt.removeFirst()
}else{
return nil
}
}
}
我們使用與對Jolt建模相似的方法對Cooler建模。我們從給Cooler定義三個屬性開始:temperature(Cooler現(xiàn)在的溫度),cansOfJolt(Cooler里Jolt的罐數(shù)),maxCans(Cooler存放的最大罐數(shù))。當我們創(chuàng)建Cooler類實例的時候,我們使用初始化器來初始化屬性。最后,我們我們?yōu)镃ooler定義了兩個操作:addJolt用來給Cooler添加Jolt,removeJolt用來從Cooler中移除Jolt?,F(xiàn)在,我們擁有了我們的Jolt和Cooler類,讓我們看看我們如何一起使用這兩個類:
var cooler = Cooler(temperature: 38.0, maxCans: 12)
for _ in 0...5{
let can = Jolt(volume: 23.5, caffeine: 280, temperature: 45)
let _ = cooler.addJolt(jolt: can)
}
let jolt = cooler.removeJolt()
jolt?.drinking(amount: 5)
print("jolt left in can: \(jolt?.volume)")
在這個例子里,我們通過初始化器創(chuàng)建了一個Cooler的實例,并且設置了默認的屬性。然后通過使用for-in循環(huán)創(chuàng)建了6個Jolt實例并加到了cooler實例中。最后,我們從cooler中取出一罐jolt,并且喝了一些。一杯清涼的jolt和jolt的咖啡因。還有比這更好的嗎?
這個設計對我們簡單的例子來說似乎很夠用。然而,它真是不靈活。雖然我真是很喜歡咖啡因,但是我的妻子不喜歡。她更喜歡Caffeine Free Diet Coke(無咖啡因健怡可樂)。在現(xiàn)有的cooler設計下,當她往Cooler添加一些Caffeine Free Diet Coke的時候,我們會告訴她那是不可能的,因為我們的Cooler只能接受Jolt。這很不好,因為正不是真實世界里cooler的工作方式。而且我不想告訴我的妻子她不能存他的Diet Coke。(相信我,沒人會想告訴她她不能存她的Diet Coke)。所以,我們如何使這個設計更加靈活?
這個問題的答案是polymorphism(多態(tài)性).polymorphism來自希臘單詞Poly和Morph。在軟件中,當我們想要在代碼里使用單一接口來展現(xiàn)多種類型的時候,我們會使用多態(tài)性。多態(tài)給了我們使用統(tǒng)一格式來與多種類型作用的能力。當我們使用統(tǒng)一的接口與不同的對象交互的時候,我們能夠隨時添加符合接口的額外對象。我們可以在我們的代碼里使用這些額外的類型,僅僅只需要一點甚至沒有改變。
使用面向對象語言,我們可以使用多態(tài),并且可以使用子類化來重用代碼。子類化就是某一個類從另一個父類派生出一個子類。例如,我們有一個從人建模出來Person類,我們可以從Person類子類化出Student類。Student類會繼承Person類的所有屬性和方法。Student類可以重寫它繼承的任何屬性和方法,也可以添加他自己額外的屬性和方法。我們也可以添加其他的派生于Person類的類,并且我們可以使用Person類的接口來給這些所有子類做交互。
當一個類從另一個類派生出來,原始的類,被稱為超類或父類,而新類被稱為子類或者。在我們的person-student例子里,Person就是超類或父類,Student就是子類。在這本書里,我們會使用父類和子類。
多態(tài)可以使用子類化來實現(xiàn),因為我們可以通過父類的接口來給所有的子類實例提供交互。舉個例子,我們有三個子類(Student,Programmer和Fireman)都繼承自Person類。那么我們可以使用Person類提供的接口來對三個子類提供交互。如果Person類提供了一個方法running(),那么我們可以確定,所有Person的子類都有一個方法叫running()(可能是來自父類的方法,也可能是來自Person類而被重寫過的)。因此,我們可以在所有子類里使用running()方法。
讓我們看看多態(tài)如何幫助我們添加Jolt以外的飲料到cooler里。在我們的原始例子里,由于Jolt以24盎司罐大小售賣,我們在對Jolt固定的罐頭的大小。(soda有不同的尺寸,但是能量飲料只賣24盎司的)。下面的枚舉器定義了我們的cooler可以接受的罐頭尺寸:
enum DrinkSize {
case Can12
case Can16
case Can24
case Can32
}
DrinkSize枚舉器讓我們可以在cooler里放置12,16,24和32盎司大小罐頭.
現(xiàn)在,讓我們看看我們我們所有飲料將要派生的基類。我們會把這個基類命名為Drink:
class Drink {
var volume : Double
var caffeine : Double
var temperature : Double
var drinkSize : DrinkSize
var description : String
init(volume: Double, caffeine:Double, temperature:Double, drinkSize:DrinkSize) {
self.volume = volume
self.caffeine = caffeine
self.temperature = temperature
self.description = "Drink base class"
self.drinkSize = drinkSize
}
func drinking(amout: Double) {
volume -= amout
}
func temperatureChange(change: Double) {
self.temperature += change
}
}
Drink類與我們原來的Jolt類很像。我們定義了與Jolt中相同的物種屬性;然而DrinkSize現(xiàn)在被定義為DrinkSize類型而不是Double。我們給Drink類定義了一個初始化方法,這個初始化方法會初始化類的所有屬性。最后,我們有兩個與我們在Jolt類里一樣的兩個方法drinking()和temperatureChange()。有一點需要注意的是,在Drink類里,我們的描述是社會成Drink base class。
現(xiàn)在,讓我們創(chuàng)建一個Drink的子類Jolt。這個類將會繼承Drink類的所有屬性和方法:
class DrinkJolt: Drink{
init(temperature: Double) {
super.init(volume: 23.5, caffeine: 280, temperature: temperature, drinkSize: DrinkSize.Can24)
self.description = "Jolt energy drink"
}
}
就Jolt類而言,我們不需要重新定義Drink類的屬性和方法。我們將會為我們的Jolt類添加一個初始化化方法。這個初始化方法只需要 Jolt罐頭需要的溫度。其他的值只需要設置成默認值就可以了。
現(xiàn)在,讓我們看看如何創(chuàng)建一個可以接受除了Jolt飲料之后的Cooler:
class DrinkCooler {
var temperature: Double
var cansOfDrinks = [Drink]()
var maxCans: Int
init(temperature: Double, maxCans: Int) {
self.temperature = temperature
self.maxCans = maxCans
}
func addDrink(drink: Drink) -> Bool {
if cansOfDrinks.count < maxCans {
cansOfDrinks.append(drink)
return true
}else{
return false
}
}
func removeDrink() -> Drink? {
if cansOfDrinks.count > 0 {
return cansOfDrinks.removeFirst()
}else{
return nil
}
}
}
新的DrinkCooler類很像原始的Cooler類,出了我們我所有的Jolt類用Drink類的參數(shù)來代替。由于Jolt類是Drink類的子類,我們可以在所有 Drink類需要的地方使用它。接下來舉個例子。下面的代碼會創(chuàng)建一個Cooler類的實例。添加6罐jolt到cooler里。從其中一罐,然后喝掉它:
var drinkCooler = DrinkCooler(temperature: 38.0, maxCans: 24)
for _ in 0...5{
let can = DrinkJolt(temperature: 45.1)
let _ = drinkCooler.addDrink(drink: can)
}
let drinkJolt = drinkCooler.removeDrink()
drinkJolt?.drinking(amout: 5)
print("Jolt Left in can: \(drinkJolt?.volume)")
在這個例子里,我們在需要Drink類實例的地方使用DrinkJolt類的實例。這就是多態(tài)。既然我們有一個有Jolt的cooler,我們準備繼續(xù)這次旅行。我妻子也想要把她的Caffeine Free Diet Coke放進去來冷藏。
我們不想剝奪她的Diet Coke,我們快速創(chuàng)建了我們可以使用cooler的CaffeineFreeDietCoke類
class CaffeineFreeDietCoke: Drink {
init(volume: Double, temperature: Double, drinkSize:DrinkSize) {
super.init(volume: volume, caffeine: 0, temperature: temperature, drinkSize: drinkSize)
self.description = "Caffeine Free Diet Coke"
}
}
CaffeineFreeDietCoke類與Jolt類非常相似。他們都是Drink類的子類,并且他們都定義了一個初始化方法來初始化這個類。關鍵在于,他們都是Drink類的子類,這意味著,我們可以把他們的實例用在cooler當中。因此,當我的妻子拿來她的Caffeine Free Diet Cokes,我們可以把他們像Jolt一樣放入cooler當中。下面的代碼示范了這個過程:
var cooler2 = DrinkCooler(temperature: 38.0, maxCans: 24)
for _ in 0...5 {
let can = DrinkJolt(temperature: 45.1)
let _ = cooler2.addDrink(drink: can)
}
for _ in 0...5{
let can = CaffeineFreeDietCoke(volume: 15.5, temperature: 45, drinkSize: DrinkSize.Can16)
let _ = cooler2.addDrink(drink: can)
}
在這個例子里,我們創(chuàng)建了一個DrinkCooler的實例。我們放入了6罐Jolt和6罐Caffeine Free Diet Coke。使用多態(tài),就像這里展示的這樣。允許我們創(chuàng)建如我們所需數(shù)量的Drink類的子類。并且,我們可以在不改變Cooler代碼的前提下,把他們添加到cooler里。這使得我們的代碼變得十分靈活。
那么,當我們從cooler里拿出一罐飲料的時候會發(fā)生什么?很明顯,當我妻子從中拿出一罐Jolt的時候,她會想要把它放回去,并且拿一罐不同的。但是,她是否知道應該拿哪一罐?
為了檢查某個實例是否是需要的類型,我們使用類型檢查方法is。如果實例類型是對的,is會返回true,反之亦然。在下面的代碼里,我們使用is來持續(xù)從cooler里移除飲料,直到我們找到Caffeine Free Diet Coke。
var foundCan = false
var wifeDrink: Drink?
while !foundCan{
if let can = cooler2.removeDrink(){
if can is CaffeineFreeDietCoke {
foundCan = true
wifeDrink = can
}else{
cooler2.addDrink(drink: can)
}
}
}
if let drink = wifeDrink {
print("Got : " + drink.description)
}
在這個代碼里,我們有一個while循環(huán)持續(xù)循環(huán)直到foundCan的值被設成true。在while循環(huán)內,我們從cooler里移除飲料,然后使用is方法來看移除的實例是不是'Caffeine Free Diet Coke類的實例。如果是,那我們就把foundCan設置成true,然后設置wifeDrink變量設置成我們剛從cooler里移除的飲料。如果這個飲料不是Caffeine Free Coke Class類的實例,那我們會把飲料放回去,讓循環(huán)返回到拿另一罐飲料。 在之前的例子里,我們展示了Swift如何被用作面向對象編程的語言。我們也是用了多態(tài)來讓我們的代碼靈活并且易擴展。然而,這種設計也有一些缺點。在我們轉向面向協(xié)議編程之前。讓我們其中兩個缺點。然后,我們會看到面向協(xié)議編程如何使得這種設計更好。 第一個缺點是,我們對于飲料(Jolt,'Caffeine Free Diet Coke和diet Coke)初始化方法的設計。當我們初始化一個子類的時候,我們需要調用父類的初始化方法。這是一把雙刃劍。當調用父類的初始化方法的時候,它會給他們一致的初始化,但是如果我們不注意,她也會給我們不恰當?shù)某跏蓟?。例如,我們使用如下代碼創(chuàng)建另一個叫做Diet Coke的飲料:
class DietCoke: Drink {
init(volume: Double, temperature: Double, drinkSize: DrinkSize) {
super.init(volume: volume, caffeine: 45, temperature: temperature, drinkSize: drinkSize)
}
}
如果我們仔細觀察,我們會看見在DietCoke類的初始化方法里,我們沒有設置description屬性。因此,這個類的描述將會是Drink base class,而這不是我們想要的
我們需要注意,當我們像這樣創(chuàng)建子類的時候,需要保證書友的屬性被正確的設置。我們不能保證,父類的初始化方法會為我們設置好所有的屬性。
這個設計的第二個缺陷是,我們使用引用類型。熟悉面向對象編程的人可能不會將其視為缺陷,并且他們在很多情況下喜歡使用引用類型。在我們的設計中,把飲料類型定義為值類型會更有意義。如果你對引用類型與值類型的工作原理不清楚,我們會在第二章的** 我們的類型選擇 **中看到他們。
當我們通過一個引用類型的實例(就是說,我們傳遞給集合中的函數(shù)或集合,如數(shù)組),我們給原始的實例傳一個引用。當我們傳遞一個值類型的實例,我們傳遞的是原始對象的新的拷貝。通過試驗一下代碼,讓我們看看如果不注意的話,使用引用類型會引發(fā)什么問題:
var jolts = [Drink]()
var myJolt = DrinkJolt(temperature: 48)
for _ in 0...5 {
jolts.append(myJolt)
}
jolts[0].drinking(amout: 10)
for (index,can) in jolts.enumerated(){
print("can \(index) amout Left:\(can.volume)")
}
在這個例子里,我們創(chuàng)建了一個包含Drink或者Drink子類的實例。然后我們創(chuàng)建了一個Jolt類,然后使用循環(huán),6次加入到數(shù)組中。下一步,我們從數(shù)組里拿出第一罐,喝了一口,然后看看數(shù)組里所有的剩余容量。這段代碼的結果如下:
can 0 amout Left:13.5
can 1 amout Left:13.5
can 2 amout Left:13.5
can 3 amout Left:13.5
can 4 amout Left:13.5
can 5 amout Left:13.5
可以看到,所有數(shù)組里的罐子里剩下的Jolt的余量都是相同的。這是因為我們創(chuàng)建了單一的Jolt實例,然后加入到jolts數(shù)組,我們給這個單一實例添加了6個引用。因此,當我們從數(shù)組里拿出第一罐,喝了一口,我們實際上是喝了數(shù)組里所有飲料。
對于有面向對象經驗的程序員來說,這樣的錯誤似乎不是問題。然而,他對初級開發(fā)者或者對不熟悉面向對象編程的開發(fā)者來說是很頭大的。這些問題更多出現(xiàn)在有復雜初始化方法的類。我們可以通過在第六章的** 在Swift里采用設計模式 **提到的生成器模式避免這個問題,或者在我們的類中實現(xiàn)copy方法可以創(chuàng)建一個實例的拷貝。
另一個面向對象編程和子類化需要注意的地方是,如同前面例子顯示的那樣,一個類只能有一個父類。例如,DrinkJolt類的父類是Drink。這將會導致一個父類變得臃腫,其代碼并不是所有子類 所需要或者想要的。這在游戲開發(fā)中是非常普遍的問題。
現(xiàn)在,讓我們看看如何使用面向協(xié)議編程實現(xiàn)drink和cooler的例子。
Swift 作為面向協(xié)議編程
對于面向對象編程,我們經常從思考對象與類的繼承來開始我們的設計。面向協(xié)議編程有點不同。這里,我們從協(xié)議開始思考設計。然而,在我們這個章節(jié)開始前,面向協(xié)議編程絕不僅僅只是協(xié)議。
通過這個部分,我們會就目前的例子,簡要討論組成面向協(xié)議編程的要素。我們會扎起下一個章節(jié)里深度討論其中的要素,它能讓你更好的理解如何在我們的應用了使用面向協(xié)議編程。
在之前的部分,我們把Swift看成一個面向對象編程的語言。我們使用類繼承的的方式來設計架構,如下圖所示:

為了使用面向協(xié)議編程重新設計它,我們需要重新思考這個設計的許多地方。第一個我們需要重新思考的是
Drink類。面向協(xié)議編程規(guī)定我們應該從一個協(xié)議二不是父類開始。這意味著我們的Drink類應當編程一個Drink協(xié)議。我們將會使用這個協(xié)議的擴展來給遵守這個協(xié)議的飲料類型添加公有的代碼。我們將在第四章中的** 所有關于協(xié)議的內容 討論協(xié)議,我們將在第五章中的 讓我們擴展一些類型 ** 介紹協(xié)議擴展。第二個我們需要重新思考的地方是使用引用類型。蘋果表示,在恰當?shù)那闆r下,最好使用值類型而不是引用類型。當我們決定使用值類型還是引用類型的時候有很多需要考慮的地方,我們將會在第二章的** 我們的類型選擇 **里討論這個問題。在這個例子里,我們將會使用將會把我們的drink類型設置為值類型(structure),并把
Cooler設置成引用類型。在這個例子里,把drink類型設置成值類型,把
Cooler設置成引用類型是基于我們將會如何使用這些類型的實例。drink類型的實例只有一個擁有者。例如,當drink在cooler里,cooler擁有它。當一個人把drink拿出來,drink就從cooler里移除,并且就它就屬于拿了它的人。Cooler類型與drink類型有點不同。drink類型一次只有一個擁有者與其作用。Cooler類型的實例則可能同時有幾個部分與其發(fā)生作用。例如,當我們代碼的某一部分往往cooler里添加drink的時候,有幾個人的實例正從cooler里拿走飲料。總結就是,我們之所以把drink類型設置成值類型,是因為,任何時候,我們的代碼里只有一個擁有者可以與drink實例發(fā)生交互。然而,
Cooler類型的對象,可以同時與我們代碼的幾個部分發(fā)生交互,所以它的類型設置成引用類型。以下部分我們會在這本書里強調多次:引用類型與值類型的一個主要不同在于,我們如何傳遞這種類型的實例。當我們傳遞一個引用類型的實例。我們傳遞的是著原實例的引用。這意味著,實例的改變會反應在兩個引用上。而當我們傳遞一個值類型的實例,我們傳遞的是原實例的新拷貝。這意味著,對一個實例的改變不會影響到另一個。
在我們進一步檢驗面向協(xié)議編程之前,我們先看看我們如何使用面向協(xié)議編程來重寫我們的例子。我們將會從創(chuàng)建
Drink協(xié)議開始:
protocol Drink{
var volume: Double {get set}
var caffeine: Double {get set}
var temperature: Double {get set}
var drinkSize: DrinkSize {get set}
var description: String {get set}
}
在我們的Drink協(xié)議里,我們定義了5個屬性,所有遵循了這個協(xié)議的類型都應該提供它們。DrinkSize的類型與之前在面向對象編程中的 DrinkSize一致。
在添加任何遵循我們的Drink協(xié)議的類型之前,我們想要拓展這個協(xié)議。協(xié)議拓展在Swift 2當中被加入,它允許我們給遵循協(xié)議的類型添加功能。這允許我們給所有遵循協(xié)議的類型定義行為而不是給它們添加行為。在我們的Drink協(xié)議的擴展里,我們定義兩個方法:drinking()和temperaturechange()。這與我們之前在面向對象編程中的Drink父類里定義的方法一樣。以下是我們在Drink拓展里的代碼:
extension Drink{
mutating func drinking(amount: Double){
volume -= amount
}
mutating func temperatureChange(change: Double){
temperature += change
}
}
現(xiàn)在,所有遵循Drink寫一點類型都會自動接收到drinking()和temperatureChange()方法。協(xié)議擴展對于遵循這個協(xié)議的類型來說,是完美的添加通用功能的方式。這與在父類當中添加功能來讓所有子類接收到這個功能的方式相似。符合協(xié)議的單獨類型也可以影響類似于超類的覆蓋功能的擴展提供的任何功能。
現(xiàn)在,讓我們創(chuàng)建Jolt和CaffeineFreeDietCoke類:
struct Jolt: Drink {
var volume: Double
var caffeine: Double
var temperature: Double
var drinkSize: DrinkSize
var description: String
init(temperature :Double) {
self.volume = 23.5
self.caffeine = 280
self.temperature = temperature
self.description = "Jolt Energy Drink"
self.drinkSize = DrinkSize.Can24
}
}
struct CaffeineFreeDietCoke: Drink {
var volume: Double
var caffeine: Double
var temperature: Double
var drinkSize: DrinkSize
var description: String
init(volume: Double, temperature :Double, drinkSize: DrinkSize) {
self.volume = volume
self.caffeine = 0
self.temperature = temperature
self.description = "Caffiene Free Diet Coke"
self.drinkSize = drinkSize
}
}
如我們所見,Jolt與CaffeineFreeDietCoke的類型是structure而不是class。這意味著,就像他們在面向對象設計中的一樣,他們都是值類型而不是引用類型。這兩種類型都在實現(xiàn)了我們在Drink協(xié)議中定義的屬性以及將用于初始化的初始化方法。
相比面向對象例子里的drink類型,我們還需要在這些類型里加入一些別的代碼。然而,我們很容易就能理解在飲料類型里發(fā)生了什么,因為所有東西都是在類型的本身初始化的,而不是在它的父類。最后,讓我們看看cooler類型:
class Cooler {
var temperature: Double
var cansOfDrinks = [Drink]()
var maxCans: Int
init(temperature: Double, maxCans: Int) {
self.temperature = temperature
self.maxCans = maxCans
}
func addDrink(drink: Drink) -> Bool {
if cansOfDrinks.count < maxCans {
cansOfDrinks.append(drink)
return true
} else{
return false
}
}
func removeDrink() -> Drink? {
if cansOfDrinks.count > 0 {
return cansOfDrinks.removeFirst()
} else{
return nil
}
}
}
如我們所見,Cooler類與本章之前在面向對象編程中創(chuàng)建的類一樣。把Cooler類創(chuàng)建為structure而不是一個類是可行的,但是它主要還是取決于我們打算如何在代碼里使用它。早前,我們說過,我們代碼的多個部分需要與cooler的一個實例交互。因此,在我們的例子里,把cooler作為引用類型比值類型要更好。
注意
蘋果的建議是在適當?shù)那闆r下優(yōu)先考慮參考類型的價值類型。因此,在有所疑惑的時候,建議我們優(yōu)先使用值類型而不是引用類型。
下圖顯示了新的設計:

現(xiàn)在,我們已經完成了重新設計,讓我們總結下什么是面向協(xié)議編程以及它與面向對象編程的不同。
總結面向協(xié)議編程與面向對象編程
我們剛剛看了Swift如何被用作面向對象編程語言和面向協(xié)議編程語言以及兩者之間的不同。在本章的例子中,這兩種設計的主要不同有兩點。
我們在面向協(xié)議編程中看到的第一點不同是,我們應該以協(xié)議而不是父類開始。我們可以使用協(xié)議擴展來給遵循協(xié)議的類型添加功能。使用面向對象編程,我們使用父類開始。當我們重新設計我們的例子的時候,我們把Drink父類轉化成Drink協(xié)議,然后使用協(xié)議擴展添加了drinking()和 temperatureChange()方法。
我們看到的第二個不同是,我們使用把drink類型定義成值類型(structures)而不是引用類型(class)。蘋果說過,在恰當?shù)那闆r下,應該使用值類型而不是引用類型。在我們的例子里,當我們事先drink類型的時候,使用值類型更合適。當然,我們仍然把Cooler定義成引用類型。
混合、匹配值類型與引用類型可能不是最好的長期維護代碼的方法。我們在例子里使用它是為了強調值類型與引用類型的不同。在第二章的我們的類型選擇里會詳細討論這點。
面向對象設計與面向協(xié)議設計都是用了多態(tài)來讓我們使用相同的接口與不同的類型交互。使用面向對象設計,我們使用父類提供的接口來與所有子類交互。在面向協(xié)議的設計里,我們使用協(xié)議和協(xié)議擴展提供的接口來與遵守協(xié)議的類型交互。
現(xiàn)在,我們總結了面向對象編程設計與面向協(xié)議編程設計的不同,讓我們再進一步看看這些不同。
面向協(xié)議編程與面向協(xié)議編程
我在本章的開頭提到過,面向協(xié)議編程絕不僅僅只包含協(xié)議。并且它不僅可以被用來寫應用,更是思考編程的一種新方式。在這個部分,我們會測試兩種設計的不同來看看上述陳述的真實意義。
作為一個開發(fā)中,我們的首要目標是開發(fā)一個好的app,但是我們也應該專注于寫一個簡介、安全的代碼。在這個部分,我們會專注于討論簡介、安全的代碼,所以讓我們看看這兩個詞意味著什么。
簡介的代碼意味著容易閱讀與理解。簡介的代碼是非常重要的,因為我們的寫的任何代碼需要被人保留下來,而這個人往往就是寫代碼的人。沒有比往回看你自己的代碼而不能理解它的時候。簡潔、易理解的代碼,也有助于更容易發(fā)現(xiàn)其中的錯誤。
安全的代碼意味著很破壞它。沒有比一下更讓一個開發(fā)者苦惱的事了:當你在代碼里做了一小點改變的時候,有不少錯誤出現(xiàn)在代碼里或者應用里出現(xiàn)很多bug。寫出簡潔的代碼,我們的代碼能夠被安全的繼承,因為其他的開發(fā)者能夠準確的理解它所表達的意思。
現(xiàn)在,讓我們簡要看看協(xié)議/協(xié)議擴展與父類的不同。我們會在第四章的關于協(xié)議的一切和第五章的讓我們擴展一些類型里討論更多相關內容。
協(xié)議和協(xié)議擴展對比父類
在面向對象編程的例子里,我們創(chuàng)建了一個從所有drink類派生出的Drink父類。而在面向協(xié)議編程的例子里,我們結合協(xié)議與協(xié)議擴展來達到相同的結果;當然,使用協(xié)議有很多的優(yōu)勢。
為了強化之前對于兩種結論的記憶,讓我們看看Drink父類與Drink協(xié)議與擴展的代碼。以下代碼顯示 了Drink父類:
class Drink {
var volume : Double
var caffeine : Double
var temperature : Double
var drinkSize : DrinkSize
var description : String
init(volume: Double, caffeine:Double, temperature:Double, drinkSize:DrinkSize) {
self.volume = volume
self.caffeine = caffeine
self.temperature = temperature
self.description = "Drink base class"
self.drinkSize = drinkSize
}
func drinking(amout: Double) {
volume -= amout
}
func temperatureChange(change: Double) {
self.temperature += change
}
}
Drink父類是我們創(chuàng)建實例的完整類型。這可能是好事,也可能是壞事。有時候,像這個例子,當我們不用創(chuàng)建父類的子類的時候;我們只需要創(chuàng)建子類的實例。這種時候,我們依然可以利用面向對象編程的協(xié)議。然而,我們還是需要使用協(xié)議擴展來添加公有的功能,這會讓我們沿著面向協(xié)議編程的路走去。
現(xiàn)在,讓我們看看我們如何使用面向協(xié)議編程來創(chuàng)建Drink協(xié)議和Drink協(xié)議擴展:
protocol Drink{
var volume: Double {get set}
var caffeine: Double {get set}
var temperature: Double {get set}
var drinkSize: DrinkSize {get set}
var description: String {get set}
}
extension Drink{
mutating func drinking(amount: Double){
volume -= amount
}
mutating func temperatureChange(change: Double){
temperature += change
}
}
兩種結論下的代碼都很安全且好理解。作為個人參考,我喜歡把實現(xiàn)從定義里分離。因此,對我而言,協(xié)議/協(xié)議擴展的代碼會更好,但這真的只是一個參考。然而,我們會在接下來的幾頁里看到協(xié)議/協(xié)議擴展作為一個一個整體來說,會是更清晰且更好理解的。
協(xié)議/協(xié)議擴展相比父類來說還有三個優(yōu)勢。第一個優(yōu)勢是,類型可以遵循多個協(xié)議;但是他們只能有一個父類。這就意味著,我們可以創(chuàng)建多個協(xié)議來添加指定功能而不是創(chuàng)建一個整體的父類。例如,對我們的Drink協(xié)議,我們也可以創(chuàng)建DietDrink、SodaDrink和EnergyDrink協(xié)議,它們包含這些飲料的指定需要和功能。然后,DietCoke和CaffeineFreeDietCoke類型要遵循Drink、DietDrink和SodaDrink協(xié)議。而Jolt結構體會遵循Drink和EnergyDrink協(xié)議。而使用父類,我們需要把DietDrink、SodaDrink和EnergyDrink協(xié)議都的內容都定義到一個單一的整體父類中。
第二個優(yōu)勢是我們可以使用協(xié)議擴展添加功能,而不需要源代碼。這意味著,我們可以擴展任意協(xié)議,即使這個協(xié)議是Swift語言本身的一部分。而為了給父類添加功能,我們需要源代碼。我們可以使用擴展給父類添加功能,但那意味著所有子類將會繼承這個功能。當然,一般情況下,我們使用擴展給指定的類添加擴展,而不是一個類的層次結構。
第三個優(yōu)勢是,協(xié)議/協(xié)議擴展可以被類、結構體、枚舉所采用,但是類的繼承被限制在類類型。協(xié)議/協(xié)議擴展給我們在恰當情況下使用值類型的選項。
實現(xiàn)drink類型
drink類型的實現(xiàn)在面向對象與面向協(xié)議里的例子是有很大差別的。我們將會看看這兩個例子的不通電。但是首先,我們需要再次惠顧代碼來提醒我們如何實現(xiàn)drink類型。我們首先看看,我們在面向對象例子里如何實現(xiàn)drink類型:
class DrinkJolt: Drink{
init(temperature: Double) {
super.init(volume: 23.5, caffeine: 280, temperature: temperature, drinkSize: DrinkSize.Can24)
self.description = "Jolt energy drink"
}
}
class CaffeineFreeDietCoke: Drink {
init(volume: Double, temperature: Double, drinkSize:DrinkSize) {
super.init(volume: volume, caffeine: 0, temperature: temperature, drinkSize: drinkSize)
self.description = "Caffeine Free Diet Coke"
}
}
這兩個類都是Drink父類的子類,他們的實現(xiàn)里面也都只有一個初始化方法。雖然這只是非常簡單且直接的實現(xiàn),我們還是需要完全理解父類被期望能夠恰當?shù)膶崿F(xiàn)他們。例如,如果我們不充分理解Drink父類,我們可能忘記設置description。在我們的例子里,忘記設置description可能不是大問題,但是在更復雜的類型了,忘記設置屬性可能導致未曾預料的行為。我們可以通過在父類的初始化方法里設置所有屬性來防止出現(xiàn)這些問題;當然,在有些情況下可能行不通。
現(xiàn)在,讓我們看看我們如何在面向協(xié)議編程例子里實現(xiàn)drink類型:
struct Jolt: Drink {
var volume: Double
var caffeine: Double
var temperature: Double
var drinkSize: DrinkSize
var description: String
init(temperature :Double) {
self.volume = 23.5
self.caffeine = 280
self.temperature = temperature
self.description = "Jolt Energy Drink"
self.drinkSize = DrinkSize.Can24
}
}
struct CaffeineFreeDietCoke: Drink {
var volume: Double
var caffeine: Double
var temperature: Double
var drinkSize: DrinkSize
var description: String
init(volume: Double, temperature :Double, drinkSize: DrinkSize) {
self.volume = volume
self.caffeine = 0
self.temperature = temperature
self.description = "Caffiene Free Diet Coke"
self.drinkSize = drinkSize
}
}
在面向協(xié)議編程例子實現(xiàn)的類型比面向對象編程里的例子要重要的多。當然,面向協(xié)議編程例子里的代碼更安全也更好理解。我們說面向協(xié)議的更安全更好理解的理由在于我們在兩個例子里如何實現(xiàn)屬性和初始化方法。
在面向對象編程例子里,所有的屬性都在父類里定義。我們需要查看代碼或者文檔看父類定義了什么屬性以及他們如何定義。而在協(xié)議里,我們也需要查看協(xié)議本身或者文檔來看如何實現(xiàn)協(xié)議,但是這個實現(xiàn)是在類型本身實現(xiàn)的。這就允許我們能夠看到這個類型的一切是怎么實現(xiàn)的,而不用查看父類的代碼或者挖掘整個的類的繼承關系來看事情是如何實現(xiàn)、初始化的。
子類的初始化方法也必須調用父類的初始化方法來保證父類的屬性被恰當?shù)脑O置。這能夠保證在子類當中有一致的初始化。它也隱藏了類是如何初始化的。在協(xié)議例子里,所有的初始化工作都是在它自身完成的。因此,我們不需要查看整個類繼承來看一切是如何初始化的。
Swift里的父類給我們的需求提供了實現(xiàn)。swift中的協(xié)議只是一個合同,表示符合給定協(xié)議的任何類型都必須滿足協(xié)議規(guī)定的請求。因此,使用協(xié)議,所有的屬性、方法和初始化方法都要在類型本身。這允許我們能夠簡單的看到所有東西是如何被定義和初始化的。
值類型vs引用類型
在引用與值類型之間有幾點本質不同,并且我們將會在第二章的我們的類型選擇里詳細討論。現(xiàn)在,我們將會把注意力集中在這兩種類型的一個主要不同:類型如何傳遞。當我們傳遞一個引用類型的實例,我們把原始對象的引用傳遞過去。這意味著,任何的變動都會反應會原始的對象。當我們傳遞一個值類型的對象,我們傳遞的是原對象的新拷貝對象。這意味著我們做的改變都不會反應到原對象上。
如同我們之前提到的那樣,在我們例子里,一個drink類型的實例一次只能有一個擁有者。不應該出現(xiàn)我們代碼的多個部分同事與drink類型的實例交互。作為例子,當我們創(chuàng)建創(chuàng)建一個drink類型的實例的時候,我們會把它放入cooler類型的一個實例。然后,如果有個人過來,并把它從冰箱里移除,這個人將會擁有這個drink的實例。如果這個人把drink給了別人,那么第二個人就會擁有這個drink。
使用值類型保證我們始終能夠拿到唯一的實例,因為我們傳的是原實例的拷貝對象而不是它的引用對象。因此,我們相信我們代碼的別的部分不會有對這個實例有意想不到的改變。這在多線程環(huán)境里尤其有用,因為不同線程可以修改數(shù)據,發(fā)生未可知的行為。
我們需要保證恰當?shù)氖褂弥殿愋团c引用類型。在這個例子里,drink類型示范了值類型應該被優(yōu)先考慮,而Coler類型示范了引用類型應當被優(yōu)先考慮。
在多數(shù)面向對象語言里,我們沒有把自定義類型設置成值類型的選項。在Swift里,類和結構體在功能上比其他語言更加接近。我們也能夠把自定義類型設置成值類型。我們只需要保證創(chuàng)建自定義類型的時候,我們使用了恰當?shù)念愋途涂梢粤?。我們將會?a href="" target="_blank">第二章的我們的類型選擇里再詳細討論這點。
勝者是...
當我們讀過這章并且看到面向協(xié)議編程相對面向對象編程的優(yōu)勢,我們可能想到面向協(xié)議編程比面向對象編程更清晰。當然,這種猜想可能并不十分準確。
面向對象編程從1970年代就出現(xiàn)了,并且它是一個被試過的真正的編程范例。面向協(xié)議編程是另一方便的新事物并且它是被設計用來修正一些面向對象編程的一些問題。我個人在一些項目里使用了面向協(xié)議編程范例,并且我對它的可能性感到很興奮。
面向對象編程和面向協(xié)議編程有相似的原理,比如從真實世界的事務里建模創(chuàng)建自定義類型并且通過多態(tài)來使用單一接口來與多種類型交互。區(qū)別在于原理的實現(xiàn)。
對我來說,項目里基于面向協(xié)議編程的代碼比使用面向對象編程的更加易于閱讀。這并不意味著我會停止使用面向對象編程。我仍然可以看到許多類的層次結構和繼承的需要。
記住,當我們設計我們的應用的時候,我們應當總是在正確的地方使用正確的工具。我們不會想要用電鋸去切一塊2x4的木塊,我們也不想要使用sklsaw(電鋸牌子)來砍倒一棵樹。因此,勝者是程序員,我們有使用不同編程范例的選擇,而不是被限制在一種上。
總結
在這章里,我們見到了Swift如何被用作面向對象編程語言,?也看到了它如何被用作面向協(xié)議編程語言。雖然,這兩種編程范例有相似的邏輯,但是他們的實現(xiàn)是不同的。
使用面向對象編程,當我們創(chuàng)建對象的時候,我們會使用類作為我們的藍圖。使用面向協(xié)議編程,我們有使用類、結構體、枚舉的選擇。我們甚至可以使用其他類型,就像我們將會在第二章的我們的類型選擇里看到的。
使用面向對象編程,我們可以使用類繼承實現(xiàn)來實現(xiàn)多態(tài)。使用面向協(xié)議編程,則使用結合協(xié)議和協(xié)議擴展的方式來實現(xiàn)多態(tài)。我們將會在第四章的關于協(xié)議的一切里深入了解協(xié)議。
使用面向對象編程,我們能夠在我們的子類中實現(xiàn)由子類繼承的功能。子類有能夠重寫來自父類的功能。使用面向協(xié)議編程,我們使用協(xié)議擴展來給遵循協(xié)議的類型添加功能。如果他們選擇的話,這些類型可以影響這個功能。我們將會在第五章的讓我們擴展一些類型里再深入探討協(xié)議擴展。
面向對象編程從1970年代就出現(xiàn)了,并且它是一個被試過的真正的編程范例。它也開始顯示出一些汗與淚水。在這章里,我們看到了其中的問題和面向協(xié)議編程被設計用來解決的設計問題。
現(xiàn)在,我們已經看過了面向協(xié)議編程的概述,是時候看看組成面向協(xié)議編程各個部分的詳情了。通過更好的理解這些不同的地方,我們能夠更好的在應用里實現(xiàn)面向協(xié)議編程。我們將會看到Swift語言提供的各種類型以及我們如何使用他們。