第一章 面向?qū)ο缶幊毯兔嫦騾f(xié)議編程

第一章 面向?qū)ο缶幊毯兔嫦騾f(xié)議編程

這本書是關(guān)于面向協(xié)議編程的。當(dāng)蘋果在 2015 年世界開發(fā)者大會上宣布 Swift 2 時,

他們也聲明 Swift 是世界上第一個面向協(xié)議編程的語言。通過它的名字, 我們可能認(rèn)為面向協(xié)議編程都是跟協(xié)議相關(guān)的; 然而, 這可能是一個錯誤的假定。面向協(xié)議編程不僅僅是關(guān)于協(xié)議; 實際上它不僅是編寫程序的新方式, 也是我們思考編程的新方式。

在這一章, 你會學(xué)到:

  • Swift 是怎么用作面向?qū)ο蟮木幊痰摹?/li>
  • Swift 是怎么用作面向協(xié)議的編程語言的。
  • 面向?qū)ο缶幊毯兔嫦騾f(xié)議編程的區(qū)別。
  • 面向協(xié)議編程相對于面向?qū)ο缶幊痰膬?yōu)點。

雖然這本書是關(guān)于面向協(xié)議編程的,我們將通過討論 Swift 是怎么用作面向?qū)ο蟮木幊陶Z言開始。理解好面向?qū)ο蟮木幊虝兄诶斫饷嫦騾f(xié)議的編程并一窺面向協(xié)議編程設(shè)計所解決的問題。

作為面向?qū)ο缶幊陶Z言的 Swift

面向?qū)ο缶幊淌且环N設(shè)計哲學(xué)。使用面向?qū)ο蟮木幊陶Z言寫程序和使用老舊的諸如 C 和 Pascal 等過程式編程語言編寫程序從根本上是不同的。過程式語言通過依靠過程(或程序)使用一系列說明來告訴計算機每一步該怎么做。然而, 面向?qū)ο蟮木幊倘渴顷P(guān)于對象的。這似乎是一個非常明顯的聲明。但是本質(zhì)上,當(dāng)我們談?wù)撁嫦驅(qū)ο缶幊痰臅r候,我們需要考慮對象。

對象是包含屬性和方法的數(shù)據(jù)結(jié)構(gòu)。對象可以是一個東西,在英語中,它們通常被當(dāng)做名詞。這些對象可以是真實世界中的對象或者虛擬的對象。如果你四處看看,你會發(fā)現(xiàn)很多真實世界中的對象,并且,實際上,它們中的所有一切都能以一種帶有屬性和動作的面向?qū)ο蟮姆绞奖荒P突?/p>

當(dāng)我開始寫這一章時,我看著外面,我看到了湖泊、很多樹、草地、我的狗、還有我家后院中的籬笆。所有這些物品都可以被模型化為含有屬性和動作的對象。

當(dāng)我寫這一章的時候,我也想起了我最喜歡的能量飲料。那種能量飲料叫做 Jolt(姑且叫它加多寶)。 不知道還有多少人記得 Jolt 碳酸水或 Jolt 能量飲料,但是我整個大學(xué)期間都在喝它。一罐加多寶(Jolt)可以被模型化為帶有屬性(容量、咖啡因量、溫度和尺寸)和動作(喝和溫度變化)的對象。

我們可以把加多寶保存在冷藏器中來使它們保持冷卻。這個冷藏器也可以被模型化為對象因為它擁有屬性(溫度、加多寶罐數(shù)、最大存儲罐數(shù))和動作(添加和移除罐子)。

正是對象讓面向?qū)ο缶幊棠菢訌姶?。使用對象,我們可以模型化真實世界中的對象?例如加多寶的罐子、或視頻游戲中的諸如字符的虛擬對象。這些對象之后可以在我們的應(yīng)用程序中交互以模型化真實世界中的行為或我們想要的虛擬世界中的行為。

在計算機程序中,我們不能在沒有能告知程序期望什么樣的屬性和動作的藍(lán)圖的情況下創(chuàng)建對象。在大部分面向?qū)ο蟮木幊陶Z言中,這個藍(lán)圖以類的形式出現(xiàn)。類是一種允許我們把對象的屬性和動作封裝到單個類型中的結(jié)構(gòu)。

我們在類中使用構(gòu)造函數(shù)(initializers)來創(chuàng)建類的實例。我們通常使用這些構(gòu)造函數(shù)來為對象設(shè)置屬性的初始值或執(zhí)行我們的類需要的其它初始化。一旦我們創(chuàng)建了類的實例,之后就能在代碼中使用它了。

關(guān)于面向?qū)ο缶幊痰乃薪忉尪己芎?,但是沒有什么比實際的代碼更能解釋這個概念了。我們來看看在 Swift 中是怎么使用類來模型化加多寶和冷藏器的。下面我們會從模型化一罐加多寶開始:

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 = "加多寶涼茶"
        self.canSize     = 24
    }

    func drinking(amount: Double)  {
        volume -= amount
    }
    func temperatureChange(change: Double) {
        temperature += change
    }
}

在這個 Jolt 類中,我們定義了 5 個屬性。這些屬性是 volume(罐子中 Jolt 的量),caffeine(罐子中有多少咖啡因),temperature(罐子的當(dāng)前溫度),description(產(chǎn)品描述),和 canSize(罐子自身的尺寸)。然后我們定義了一個用于初始化對象屬性的構(gòu)造函數(shù)。該構(gòu)造函數(shù)會確保所有的屬性在實例被創(chuàng)建后都被合適地初始化了。最后,我們?yōu)? can 定義了兩個動作。這兩個動作是 driking(有人喝罐子中的飲料時調(diào)用)和 temperatureChange(罐子的溫度變化時調(diào)用)。

現(xiàn)在,我們看看怎么模型化一個冷藏器以使我們的加多寶罐子保持冷藏,因為沒有人喜歡加熱的加多寶罐子:

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
        }
    }
}

我們以和模型化加多寶罐類似的方法模型化了冷藏器。我們以定義 3 個冷藏器的屬性開始。那三個屬性是 temperature(冷藏器中的當(dāng)前溫度)、cansOfJolt(冷藏器中加多寶的罐數(shù))和 maxCans(冷藏器能裝下地最大罐數(shù))。當(dāng)我們創(chuàng)建 Cooler 類的實例時,我們使用一個構(gòu)造函數(shù)來初始化屬性。最后,我們?yōu)槔洳仄鞫x了兩個動作。它們是 addJolt(用于為冷藏器添加加多寶罐)或 removeJolt(用于從冷藏器中移除加多寶罐)。既然我們擁有了 Jolt 和 Cooler 類,讓我們看看怎么把這兩個類組合到一塊使用:

var cooler = Cooler(temperature: 38.0, maxCans: 12)

for _ in 1...5 {
    let can = Jolt(volume: 23.5, caffeine: 280, temperature: 45)
    let _   = cooler.addJolt(can)
}

let jolt = cooler.removeJolt()
jolt?.drinking(5)
print("罐子中還剩 \(jolt?.volume) ml 加多寶")

在這個例子中,我們使用構(gòu)造函數(shù)來設(shè)置默認(rèn)屬性創(chuàng)建了 Cooler 類的一個實例。當(dāng)我們創(chuàng)建了 Jolt 類的 6 個實例并使用 for-in 循環(huán)把它們添加到冷藏器中。最后,我們從冷藏器中拿出一罐加多寶并喝了一點。還有比這更爽的嗎?

這種設(shè)計對于我們這個簡單地例子似乎工作的很好; 然而,它真的不是那么靈活。雖然我真的喜歡咖啡因,但是我妻子不喜歡;她更喜歡不含咖啡因的減肥可樂。如果使用我們當(dāng)前的設(shè)計,當(dāng)她要在冷藏器中添加一些她的減肥可樂時,我們不得不告訴她那是不可能的因為我們的冷藏器只接受 Jolt 實例。這不好,因為現(xiàn)實中冷藏器不是這么工作的,因為我不會告訴妻子她不能把減肥可樂放進(jìn)冷藏器中(相信我沒有人會愿意告訴他的妻子她不能把減肥可樂放進(jìn)冷藏器中,不然等著看吧,她一整天都會唧唧歪歪的!) 所以,我們怎么使這個設(shè)計更加靈活呢?

答案是多態(tài)。多態(tài)來源于希臘單詞 Poly(多的意思)和 Morph(形式)。在計算機科學(xué)中,當(dāng)我們想在代碼中使用單個接口來表示多個類型時我們使用多態(tài)。多態(tài)讓我們擁有了以唯一的方式和多個類型進(jìn)行交互的能力, 我們能在任何時候添加遵守那個接口的額外的對象類型。然后我們可以在代碼中幾乎不做更改地使用這些額外的類型。

使用面向?qū)ο缶幊陶Z言,我們能使用子類化來達(dá)到多態(tài)和代碼復(fù)用。子類化就是一個類繼承自它的父類。例如, 假如我們有一個模型化普通人的 Person 類, 然后我們可以子類化 Person 類來創(chuàng)建 Student 類。則 Student 類會繼承 Person 類的所有屬性和方法。 Student 類可以覆蓋任何它繼承到的屬性和方法并且/或者添加它自己的額外的屬性和方法。之后我們能添加派生于 Person 超類的額外的類,并且我們能通過 Person 類呈現(xiàn)的接口來跟所有這些子類進(jìn)行交互。

當(dāng)一個類派生自另一個類,那么原來的類,即我們派生新類的那個類,就是人們熟知的超類或父類,派生出的新類就是人們所熟知的孩子或子類。在我們的 person-student 例子中, Person 類是超類或父類, 而 Student 是子類或孩子類。 在這本書中,我們會一直使用屬于超類和子類。

多態(tài)是使用子類化達(dá)到的因為我們能通過超類呈現(xiàn)的接口和所有子類的實例進(jìn)行交互。舉個例子,如果我們有 3 個孩子類(Student,Programmer,和 Fireman)都是 Person 類的子類,那么我們能夠通過 Person 類呈現(xiàn)的接口跟所有 3 個子類進(jìn)行交互。如果 Person 類擁有一個名為 running() 的方法,那么我們可以假定 Person 類的所有子類都擁有一個名字 running()的方法(這個方法要么繼承自 Person 要么是子類中的方法覆蓋了 Person 類的中的方法)。因此,我們可以使用 running() 方法跟所有子類進(jìn)行交互。

讓我們來看看多態(tài)是怎么幫助我們把 drinks 而非 Jolt 添加到我們的冷藏器中的。在我們原來的例子中,我們可以在 Jolt 類中硬編碼罐子的尺寸因為 Jolt 能量飲料只有 24 盎司的罐子賣(蘇打有不同的尺寸,但是能量飲料只賣 24 盎司的)。下面的枚舉定義了冷藏器將接受的罐子尺寸:

enum DrinkSize {
    case Can12
    case Can16
    case Can24
    case Can32
}

這個 DrinkSize 枚舉讓我們在冷藏器中使用 12、16、24 和 32 盎司的飲料尺寸。

現(xiàn)在, 讓我們看看我們的基類或超類,所有的 drink 類型都會派生自它。我們會把這個超類叫做 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.drinkSize    = drinkSize
        self.description  = "飲料基類"
    }

    func drinking(amount: Double) {
        volume -= amount
    }
    func temperatureChange(change: Double) {
        self.temperature += change
    }
}

這個 Drink 類很像原來的 Jolt 類。我們定義了原 Jolt 類中同樣擁有的 5 個屬性;然而,drinkSize 現(xiàn)在被定義為 DrinkSize 類型而非 Double 類型。我們?yōu)?Drink 類型定義了單個構(gòu)造函數(shù)以初始化類中的所有 5 個屬性。最后, 我們擁有了和原來的 Jolt 類相同的兩個方法,它們是 drinking()temperatureChange()。需要注意的一件事情是,在 Drink 類中,我們的 description 屬性被設(shè)置到 Drink 基類中了。

現(xiàn)在,我們創(chuàng)建一個 Jolt 類成為 Drink 類的子類。這個類將從 Drink 類中繼承所有的屬性和方法:

class Jolt: Drink {
    init(temperature: Double) {
        super.init(volume: 23.5, caffeine: 280,
        temperature: temperature, drinkSize: DrinkSize.Can24)
        self.description = "加多寶能量飲料"
    }
}

就像我們在 Jolt 類中看到的那樣,我們不需要重新定義繼承自超類 Drink 的屬性和方法。我們給 Jolt 類添加了一個構(gòu)造函數(shù)。這個構(gòu)造函數(shù)要求提供 Jolt 罐子的溫度。所有其他的值被設(shè)置為它們給 Jolt 罐提供的默認(rèn)值。

現(xiàn)在, 我們看看怎么創(chuàng)建 Cooler 類來接受除了 Jolt 類型之外的其他飲料類型:

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 類很像原來的那個 Cooler 類,除了我們把所有對 Jolt 類的引用替換成了對 Drink 類的引用。因為 Jolt 類是 Drink 類的一個子類,我們可以把 Jolt 類用在需要 Drink 實例的任何地方。我們來看看這是怎么工作的。下面的代碼會創(chuàng)建一個 Cooler 類。然后向冷藏器中添加 6 罐加多寶(Jolt),再從冷藏器中拿出一罐加多寶來喝:

var cooler = Cooler(temperature: 38.0, maxCans: 24)

for _ in 0...5 {
    let can = Jolt(temperature: 45.1)
    let _   = cooler.addDrink(can)
}

let jolt = cooler.removeDrink()
cooler.cansOfDrinks.count
jolt?.drinking(5)
print("這罐加多寶還剩 \(jolt?.volume) 盎司")

注意在這個例子中, 在需要 Drink 類實例的地方我們使用了 Jolt 類的實例。這就是多態(tài)。既然我們冷藏器中有了 Jolt,我們將要開始我們的旅途了。我妻子當(dāng)然想帶上她的不含咖啡因的減肥可樂,所以她問我能否在冷藏器中放上一些以保持冷藏。我知道我們阻止不了她喝減肥可樂,所以我們很快創(chuàng)建了一個可以使用的 CaffeineFreeDietCoke 類。這個類的代碼如下:

class CaffeineFreeDietCoke: Drink {
    init(volume: Double, temperature: Double, drinkSize: DrinkSize) {
        super.init(volume: volume,  caffeine: 0, temperature: temperature, drinkSize: drinkSize)
        self.description  = "不含咖啡因的減肥可樂"
    }
}

CaffeineFreeDietCoke 類和 Jolt 類很像。 它們都是 Drink 類的子類,并且它們都定義了一個構(gòu)造函數(shù)來初始化類。關(guān)鍵是它們都是 Drink 類的子類,這意味著在冷藏器中這兩個類的實例我們都可以使用。因此, 當(dāng)我的妻子帶了 6 瓶不含咖啡因的減肥可樂時,我們可以把它們放進(jìn)冷藏器中就像我們存放加多寶罐子一樣。下面的代碼解釋了這個:

var cooler = Cooler(temperature: 38.0, maxCans: 24)
for _ in 0...5 {
    let can = Jolt(temperature: 45.1)
    let _   = cooler.addDrink(can);
}

for _ in 0...5 {
    let can = CaffeineFreeDietCoke(volume: 15.5, temperature: 45, drinkSize: DrinkSize.Can16)
    let _   = cooler.addDrink(can)
}

在這個例子中, 我們創(chuàng)建了一個冷藏器(cooler)實例;我們在冷藏器中放進(jìn)了 6 罐加多寶(Jolt)和 6 罐不含咖啡因的減肥可樂。使用多態(tài),就像這兒展示的,允許我們創(chuàng)建盡可能多的 Drink 子類, 并且所有這些子類都可以用在 Cooler 類中而不需更改 Cooler 類的代碼。這讓我們的代碼更加靈活。

所以,當(dāng)我們從冷藏器中拿走一個罐子會發(fā)生什么? 顯然地,如果我妻子拿到一罐加多寶,她會把它放回去并拿走另外不同的一罐。但是她怎么知道她拿到的是什么呢?

為了檢查某個實例是哪個特定的類型,我們使用類型檢查操作符(is)。如果實例的類型是那個類型則類型檢查操作符返回 true, 否則就返回 false。在下面的代碼中,我們使用類型檢查操作符來不斷地從冷藏器中移除罐子直到我們找到不含咖啡因的減肥可樂為止:

var foundCan = false
var wifeDrink: Drink?

while !foundCan {
    if let can = cooler.removeDrink() {
        if can is CaffeineFreeDietCoke {
            foundCan = true
            wifeDrink = can
        } else {
          cooler.addDrink(can)
        }
    }
}

if let drink = wifeDrink {
    print("拿到了 " + drink.description)
}

在這個代碼中,我們的 while 循環(huán)持續(xù)循環(huán)直到 foundCan 布爾值被設(shè)置為 true。 在 while 循環(huán)中,我們從冷藏器中拿出一罐飲料然后使用類型檢查操作符(is)來看我們拿出的罐子是否是 CaffeineFreeDietCoke 類的實例。如果它是 CaffeineFreeDietCoke 類的實例,那么我會把 foundCan 布爾值設(shè)置為 true 并把 wifeDrink 變量設(shè)置為剛從冷藏器中拿走的那罐飲料的實例。 如果那罐飲料不是 CaffeineFreeDietCoke 類的實例,那么我們會把罐子放回到冷藏器中并回到循環(huán)中以抓取另外一罐飲料。

在上面的例子中, 我們展示了 Swift 是怎么用作面向?qū)ο蟮木幊陶Z言的。我們還使用了多態(tài)來讓我們的代碼更加靈活并且更容易擴展;然而,這個設(shè)計有幾個缺點。在我們進(jìn)行面向協(xié)議編程之前,我們來看看這兩個缺點。然后,我們會看到面向協(xié)議編程是怎么讓這個設(shè)計更好的。

我們的設(shè)計中的第一個缺點是 drink類(Jolt, CaffeineFreeDietCoke 和 DietCoke) 的初始化。當(dāng)我們初始化子類的時候,我們需要調(diào)用超類的構(gòu)造函數(shù)。這是把雙刃劍。雖然調(diào)用我們超類的構(gòu)造函數(shù)讓我們擁有了一致性的初始化,但是如果我們不小心的話,它也會帶給我們不合適的初始化。例如,假設(shè)說我們使用如下代碼創(chuàng)建了另外一個叫做 DietCokeDrink 類:

class DietCoke: Drink {
    init(volume: Double, temperature: Double, drinkSize: DrinkSize) {
        super.init(volume: volume, caffeine: 45, temperature: temperature, drinkSize: drinkSize)
    }
}

如果我們仔細(xì)看, 我們會看到在 DietCoke 類中的構(gòu)造函數(shù)中, 我們根本沒有設(shè)置 description 屬性。因此,這個類的 description 會使用 Drink 基類中的 description, 這不是我們想要的。

當(dāng)我們創(chuàng)建這樣的子類的時候要小心以確保所有的屬性被合理的設(shè)置了, 我們不能指望超類的構(gòu)造函數(shù)會為我們合理地設(shè)置所有的屬性。

我們的設(shè)計的第二個缺點是我們使用了引用類型。 雖然那些熟悉面向?qū)ο缶幊痰娜丝赡苷J(rèn)為這不是一個缺點并且很多情況下都偏好使用引用類型,在我們的設(shè)計中,把 drink 的類型定義為值類型更有意義。如果你不熟悉引用類型和值類型是如何工作的,我們會在第二章,我們的類型選擇里深入討論它們。

當(dāng)我們傳遞引用類型(即我們傳遞給函數(shù)或像數(shù)組那樣的集合)時,我們傳遞的時對原實例的引用。當(dāng)我們傳遞值類型的實例時,我們傳遞的是對原實例的一份拷貝。通過實驗下面的代碼,我們來看看如果我們不小心地使用了引用類型會導(dǎo)致什么問題:

var jolts  = [Drink]()
var myJolt = Jolt(temperature: 48)
for _ in 0...5 {
    jolts.append(myJolt)
}

jolts[0].drinking(10)

for (index, can) in jolts.enumerate() {
    print("Can \(index) amount Left: \(can.volume)")
}

在這個例子中,我們創(chuàng)建了一個會包含 Drink 類或 Drink 類的子類的實例的數(shù)組。之后我們創(chuàng)建了一個 Jolt 類的實例并在數(shù)組里放了 6 罐加多寶。接著,我們從數(shù)組中拿出第一罐來喝并打印出數(shù)組中每罐加多寶的剩余容量。如果我們運行這段代碼,我們會看到如下結(jié)果:

Can 0 amount Left: 13.5
Can 1 amount Left: 13.5
Can 2 amount Left: 13.5
Can 3 amount Left: 13.5
Can 4 amount Left: 13.5
Can 5 amount Left: 13.5

就像我們從結(jié)果中看到的,數(shù)組中的所有加多寶罐子都擁有同樣的剩余容量。這是因為我們創(chuàng)建了 Jolt 類的單個實例,之后我們在 jolts 數(shù)組中添加了 6 個該單個實例的引用。因此,我們從數(shù)組中拿出第一罐飲料時來喝時,我們實際上把數(shù)組中的每一罐都拿出來喝了。

這種錯誤對于有經(jīng)驗的面向?qū)ο蟮某绦騿T看起來不是什么問題;然而,它經(jīng)常出現(xiàn)在不熟悉面向?qū)ο缶幊痰某跫壋绦騿T或開發(fā)者之中很令人吃驚。當(dāng)類的構(gòu)造函數(shù)很復(fù)雜時這種錯誤出現(xiàn)的更加頻繁。我們可以通過使用第六章,在 Swift 中遵循設(shè)計模式中看到的 Builder 模式來避免這個問題,或者在我們的自定義類中實現(xiàn)一個 copy 方法以拷貝一份實例。

就像上面的例子中展示的那樣,面向?qū)ο缶幊毯妥宇惢枰⒁獾牧硗庖患虑槭牵?一個類只能擁有一個超類。例如,我們的 Jolt 類的超類是 Drink 類。這可能導(dǎo)致單個超類變得非常臃腫并且包含所有子類中所不需要或不想要的代碼。這在游戲開發(fā)中是一個普遍的問題。

現(xiàn)在, 我們來看看怎么使用面向協(xié)議的編程來實現(xiàn)我們的 drinks 和 cooler 例子。

作為面向協(xié)議編程語言的 Swift

使用面向?qū)ο缶幊蹋覀冊谠O(shè)計時通常從考慮對象和類的層級開始。面向協(xié)議編程有點不同。這兒, 我們在設(shè)計時從考慮協(xié)議開始。然而,就像我們在這一章的開頭所說的,面向協(xié)編程不僅僅是關(guān)于協(xié)議的。

當(dāng)我們?yōu)g覽這一節(jié)時, 我們會就我們當(dāng)前的例子簡要討論下組成面向協(xié)議編程的不同條目。然后我們會在接下來的幾章里深入探討這些條目以讓你更好地理解在我們的應(yīng)用程序中是怎么完整地使用面向協(xié)議編程的。

在之前的小節(jié)中,當(dāng)我們把 Swift 用作面向?qū)ο蟮木幊陶Z言時, 我們使用類的層級來設(shè)計我們的方案, 就像下面的展示圖一樣:

為了用面向協(xié)議編程重新設(shè)計這個方案,我們需要重新思考該設(shè)計的幾個方面。第一個方面是我們怎么重新考慮 Drink 類。面向協(xié)議編程聲明我們應(yīng)該從協(xié)議而不是超類開始。這意味著我們的 Drink 類會變成 Drink 協(xié)議。我們可以使用協(xié)議擴展為遵守該協(xié)議的 drink 類添加通用代碼。我們將在第四章關(guān)于協(xié)議中復(fù)習(xí)協(xié)議,并且我們會在第五章讓我們擴展某些類型中涵蓋協(xié)議擴展。

我們要重新思考的第二個方面是引用(類)類型的使用。在 Swift 中,蘋果已經(jīng)聲明了在盡可能合理的地方更偏好使用值類型勝過使用引用類型。是使用引用類型還是使用值類型有很多地方需要考慮,我們會在第二章中深入了解這個問題。在這個例子中,我們會在 drink類型(Jolt 和 CaffeineFreeDietCoke)中使用值(結(jié)構(gòu)體)類型,在 Cooler 類型中使用引用(類)類型。

在這個例子中,為 drink 類型使用值類型和為 Cooler 類型使用引用類型的決定依賴于我們怎么使用這些類型的實例。我們的 drink 類型的實例只會有一個擁有者。例如,當(dāng)飲料在冷藏器中時,冷藏器就擁有了它。但是之后,當(dāng)有人把它從冷藏器中拿了出來,這個人就擁有了它。

Cooler 類型和 drink 類型有點不一樣。雖然 drink 類型一次只會有一個擁有者和它交互,但是 Cooler 類型可能在代碼中擁有幾個部分來跟它進(jìn)行交互。例如,我們可以讓代碼的一部分為冷藏器添加飲料而讓幾個人的實例從冷藏器中喝飲料。

總的來說, 我們使用值類型(結(jié)構(gòu)體)來模型化我們的 drink 類型因為一次只有代碼的一部分能跟 drink 類型的實例進(jìn)行交互。然而,我們使用引用類型(類)來模型化 Cooler 因為我們的代碼的多個部分將和 Cooler 類型的同一個實例進(jìn)行交互。

我們會在該書中多次強調(diào)這點:引用類型和值類型的一個主要區(qū)別是我們怎么傳遞類型的實例。當(dāng)我們傳遞引用類型的實例時, 我們傳遞的是對原實例的引用。這意味著變化被反射到這兩個引用中。當(dāng)我們傳遞值引用的時候,我們傳遞的是對原實例的一份拷貝。這意味著一個實例中的更改不會反射到其他實例中。

在我們你一步實驗面向協(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é)議中, 我們定義了每個遵守該協(xié)議的類型必須要提供的 5 個屬性。 DrinkSize 類型和我們這一章中面向?qū)ο笮」?jié)中的 DrinkSize 一樣。

在我們添加遵守 Drink 協(xié)議的任何類型之前,我們想擴展一下這個協(xié)議。協(xié)議擴展在 Swift 2 中被添加進(jìn)來,它允許我們?yōu)樽袷卦搮f(xié)議的類型提供功能。這讓我們?yōu)樽袷卦搮f(xié)議的所有類型定義行為,而不是把行為添加到每個遵守該協(xié)議的單獨的類型中。在 Drink 協(xié)議的擴展中,我們會定義兩個方法: drinking()temperatureChange()。 這個這一章中的面向?qū)ο缶幊讨械?Drink 超類中的兩個方法相同。下面是 Drink 擴展的代碼:

extension Drink {
    mutating func drinking(amount: Double) {
        volume -= amount
    }
    mutating func temperatureChange(change: Double) {
        temperature += change
    }
}

現(xiàn)在,任何遵守 Drink 協(xié)議的類型都會自定地接收到 drinking() 方法和 temperatureChange 方法。協(xié)議擴展很適合為遵守協(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 = "加多寶能量飲料"
        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 = "不含咖啡因的減肥可樂"
        self.drinkSize   = drinkSize
    }
}

如我們所見, JoltCaffeineFreeDietCoke 類型都是結(jié)構(gòu)體而非類。這意味著它們都是值類型而非引用類型,就像它們在面向?qū)ο笤O(shè)計中一樣。這兩個類型都實現(xiàn) Drink 協(xié)議中定義的 5 個屬性還有一個構(gòu)造函數(shù)用于初始化類型的實例。和面向?qū)ο蟮?drink 類的例子相比,這些類型需要的代碼更多。然而,在這里很容易理解這些 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 類和我們在這一章的面向?qū)ο缶幊桃还?jié)中所創(chuàng)建的類相同。對于把 Cooler 類型創(chuàng)建為結(jié)構(gòu)體而非類會頗有微詞, 但是它實際上取決于我們在代碼中打算怎么用它。之前, 我們聲明代碼的多個部分會需要跟單個 cooler 實例進(jìn)行交互。因此,最好把我們的 cooler 實現(xiàn)為引用類型而非值類型。

注意

蘋果的建議是在盡可能合理地地方優(yōu)先使用值類型而非引用類型。因此,疑惑的時候,推薦以值類型開始而非引用類型。

下面的示意圖展示了我們的新設(shè)計看起來什么樣:

img

既然我們完成了重新設(shè)計,讓我們來總結(jié)一下面向協(xié)議編程和面向?qū)ο缶幊讨g有什么不同吧。

總結(jié)面向協(xié)議編程和面向?qū)ο缶幊?/h3>

我們剛剛看到了 Swift 是怎么既用作面向?qū)ο蟮木幊陶Z言又用作面向協(xié)議的編程語言的,還有兩者真正的不同之處。在這一章展示的例子中, 這兩種設(shè)計之間有兩個主要的不同之處。

第一個不同之處是當(dāng)我們談?wù)撁嫦騾f(xié)議編程的時候應(yīng)該從協(xié)議開始而非從超類開始。然后我們可以使用協(xié)議擴展來為遵守該協(xié)議的類型添加功能。而對于面向?qū)ο缶幊蹋覀儚某愰_始。在我們重新設(shè)計我們的例子的時候,我們把 Drink 超類轉(zhuǎn)換為 Drink 協(xié)議,然后使用協(xié)議擴展以添加 drinking() 方法和 temperatureChange() 方法。

我們看到的第二個實際的區(qū)別是我們的 drink 類型使用了值類型(結(jié)構(gòu)體)而非引用類型(類)。蘋果已經(jīng)說了我們應(yīng)該在合適的地方盡可能地偏好使用值類型而非引用類型。在我們的例子中,當(dāng)我們實現(xiàn)我們的 drink 類型時,使用值類型是合適的;然而,我們?nèi)耘f把 Cooler 類型實現(xiàn)為引用類型。

對于我們的代碼的長期維護(hù)混合和匹配值類型和引用類型可能并不是最好的方法。我們在例子中使用它使為了強調(diào)值類型和引用類型之間的不同。在第二章我們的類型選擇中,我們會詳細(xì)討論這個。

面向?qū)ο蟮脑O(shè)計和面向協(xié)議的設(shè)計都使用了多態(tài)讓我們使用同樣的接口來跟不同的類型進(jìn)行交互。在面向?qū)ο蟮脑O(shè)計中,我們使用了超類提供的接口來跟所有的子類進(jìn)行交互。在面向協(xié)議的設(shè)計中,我們使用了協(xié)議和協(xié)議擴展提供的接口來跟遵守該協(xié)議的類型進(jìn)行交互。

既然我們已經(jīng)總結(jié)了面向?qū)ο缶幊淘O(shè)計和面向協(xié)議編程設(shè)計之間的區(qū)別,讓我們走得更近一點來看看兩者之間的區(qū)別。

面向?qū)ο缶幊毯兔嫦騾f(xié)議編程

我在這一章的開頭提到面向協(xié)議編程不僅僅是關(guān)于協(xié)議的,而且也是一種新的編寫程序和編程思考方式。在這一節(jié)里,我們會驗證兩種設(shè)計模式的不同之處,并看看那個聲明意味著聲明。

作為一個開發(fā)者,我們主要的目標(biāo)是開發(fā)好一個應(yīng)用程序并且工作良好,但是我們也應(yīng)該著眼于寫干凈并安全的代碼。在這一節(jié)中,我們會談?wù)摵芏嚓P(guān)于寫干凈和安全的代碼的東西,所以讓我們通過這些條目來查看我們的意思是什么。

干凈的代碼是非常易讀和易理解的代碼。寫出干凈的代碼很重要,因為我們寫得任何代碼都需要由某人維護(hù),而那個人通常是寫出那些代碼的人。沒有什么比回頭看你所寫的代碼但是又不理解它干什么更糟糕的了干凈的代碼更容易找出代碼中得錯誤并且容易理解。

關(guān)于安全的代碼我們的意思是代碼很難被中斷。作為一個開發(fā)者沒有什么比在代碼中做一處小的修改并導(dǎo)致代碼庫中到處是錯誤或讓應(yīng)用程序出現(xiàn)很多 bugs 更令人沮喪的了。通過書寫干凈的代碼,我們的代碼將會天然地更安全因為其他開發(fā)者能查看代碼并準(zhǔn)確地知道它做了什么。

現(xiàn)在,我們簡要地看看協(xié)議/協(xié)議擴展和超類之間的區(qū)別。我們會在第四章-協(xié)議的全部和第五章-擴展某些類型中涵蓋更多有關(guān)的東西。

協(xié)議和協(xié)議擴展 VS. 超類

在面向?qū)ο缶幊痰睦又?,我們?chuàng)建了一個 Drink 超類,從這個超類派生出所有的 drink 類。在面向協(xié)議編程的例子中,我們結(jié)合使用了協(xié)議和協(xié)議擴展來達(dá)到同樣地結(jié)果;然而,使用協(xié)議有幾個優(yōu)點。

刷新一下我們對兩種法案的記憶,我們來看看 Drink 超類和 Drink 協(xié)議和協(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 = "飲料基類"
        self.drinkSize   = drinkSize
    }
    
    func drinking(amount: Double) {
        volume -= amount    
    }
    func temperatureChange(change: Double) {
        temperature += change
    }
}

Drink 類是一個我們能創(chuàng)建實例的完備類型。這是件好事也是件壞事。有的時候,就像這個例子中的,這個時候我們不應(yīng)該創(chuàng)建超類的實例;我們應(yīng)該只創(chuàng)建子類的實例。為此,我們?nèi)耘f可以在面向?qū)ο缶幊讨惺褂脜f(xié)議;然而,我們?nèi)匀恍枰褂脜f(xié)議擴展來添加通用的功能,這會導(dǎo)致我們向下沿著面向協(xié)議編程的路徑繼續(xù)。

現(xiàn)在, 我們來看看怎么在 Drink 協(xié)議和 Drink 的協(xié)議擴展中使用面向協(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é)議擴展相比超類有 3 個優(yōu)點:第一個優(yōu)點是類型可以遵守多個協(xié)議而只能擁有一個超類。這意味著我們可以創(chuàng)建很多個協(xié)議以添加每個特定的功能而不是把所有功能都寫到一個巨大的類中。例如,使用我們的 Drinks 協(xié)議,我們還能創(chuàng)建包含特定需求和功能的 DietDrink、SodaDrink、和 EnergyDrink 協(xié)議。然后,DietCokeCaffeineFreeDietCoke 類型會遵守 Drink、DietDrinkSodaDrink 協(xié)議,而 Jolt 結(jié)構(gòu)體會遵守 DrinkEnergyDrink 協(xié)議。使用超類, 我們需要把定義在 DietDrink、SodaDrink 和 EnergyDrink 協(xié)議中得功能組合到單個巨大的超類中。

第二個優(yōu)點是我們不需要源代碼就可以使用協(xié)議擴展來添加功能。這意味著我們可以擴展任何協(xié)議,即使是 Swift 自己內(nèi)建的協(xié)議也能擴展。而給超類添加功能我們需要知道源代碼。我們可以使用擴展給超類添加功能,這意味著所有的子類會繼承那個功能。然而,我們一般使用擴展來為特定的類添加功能而非為類的層級添加功能。

第三個優(yōu)點是協(xié)議可以被類、結(jié)構(gòu)體和枚舉遵守,而類層級被約束為類類型。協(xié)議/協(xié)議擴展讓我們可以在盡可能合理的地方使用值類型。

實現(xiàn) drink 類型

首先看一下面向?qū)ο蟮膶崿F(xiàn):

class Jolt: Drink {
    init(temperature: Double) {
        super.init(volume: 23.5, caffeine: 280, temperature: temperature, drinkSize: DrinkSize.Can24)
        self.description = "加多寶能量飲料"
    }
}

class CaffeineFreeDietCoke: Drink {
  init(volume: Double, temperature: Double, drinkSize: DrinkSize) {
      super.init(volume: volume, caffeine: 0, temperature: temperature, drinkSize: drinkSize)
      self.description = "不含咖啡因的減肥可樂"
  }
}

這兩個都是 Drink 超類的子類并且都實現(xiàn)了單個構(gòu)造函數(shù)。我們需要完全理解超類期望它們合理地去實現(xiàn)什么。例如,如果我們沒有完全理解超類,我們可能會忘記設(shè)置 description 屬性。在我們的例子中,忘記設(shè)置超類可能沒有什么大問題,但是在更復(fù)雜的類型中,忘記合理地設(shè)置某個屬性可能會導(dǎo)致意想不到的行為。我們可以在超類的構(gòu)造函數(shù)中設(shè)置所有的屬性來阻止這些錯誤;然而,這在某些情況下可能不行。

現(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 = "加多寶能量飲料"
        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 = "不含咖啡因的減肥可樂"
      self.drinkSize   = drinkSize
  }
}

面向協(xié)議編程的例子更易懂而且更安全。在面向?qū)ο缶幊痰睦又?,所有的屬性都定義在超類中。我們需要查看超類的代碼或文檔來看看超類中定義了哪些屬性,并且這些屬性是怎么定義的。使用協(xié)議,我們也需要查看協(xié)議自身或協(xié)議的文檔來查看要實現(xiàn)的屬性,但是實現(xiàn)是在類型自身中做的。這允許我們在類型自身中查看所實現(xiàn)的所有東西而不需要來回查看超類的代碼,或者穿梭于類的層級來查看東西是怎么實現(xiàn)和初始化的。

子類中的構(gòu)造函數(shù)也必須調(diào)用超類的構(gòu)造函數(shù)以確保超類的所有屬性都被合理地設(shè)置了。雖然這的確保證了子類之間初始化的一致性,但是它也隱藏了類是如何初始化的。在面向協(xié)議的例子中,所有的初始化都是在類型自身中完成的。因此,我們不需要在類的層級之間來回穿梭以查看所有東西是如何初始化的。

Swift 中的超類提供了我們要求的實現(xiàn)。Swift 中的協(xié)議僅僅是一個約定,任何遵守給定協(xié)議的類型必須填充協(xié)議指定的需求。因此,使用協(xié)議,所有的屬性、方法和構(gòu)造函數(shù)都被定義在遵守協(xié)議的類型自身中。這讓我們很容易地查看到所有東西是怎么被定義和初始化的。

值類型 VS. 引用類型

引用類型和值類型的一個主要的區(qū)別就是類型是如何傳遞的。當(dāng)我們傳遞引用類型(class)的實例時,我們傳遞的對原實例的引用。這意味著所做的任何更改都會反射回原實例中。當(dāng)我們傳遞值類型的實例時,我們傳遞的是對原實例的一份拷貝。這意味著所做的任何更改都不會反射回原實例中。

我們之前提到過,在我們的例子中, drink 類型的實例一次應(yīng)該只有一個擁有者。沒有必要在代碼中得多個部分中和單個 drink 類型進(jìn)行交互。舉個例子,當(dāng)我們創(chuàng)建了一個 drink 類型,我們會把它放進(jìn)冷藏器類型實例中。然后,如果有人過來了然后從冷藏器中拿走了一罐飲料,那么這個人會擁有那個飲料實例。如果這個人把飲料給了另外一個人,那么第二個人會擁有那罐飲料。

使用值類型確保了我們總是得到一個唯一的實例因為我們傳遞了一份對原實例的拷貝而非對原實例的引用。因此,我們能相信沒有代碼中的其它部分會意外地修改我們的實例。這在多線程環(huán)境中尤其有用,其中不同的線程可以修改數(shù)據(jù)并創(chuàng)建意外地行為。

贏家是 ...

沒有誰贏誰輸,使用合適的工具做正確的事情。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容