来自 电脑系统 2019-09-19 12:07 的文章
当前位置: 金沙澳门官网网址 > 电脑系统 > 正文

金沙澳门官网网址面向对象编程和面向协议编程

这本书是关于面向协议编程的。当苹果在 2015 年世界开发者大会上宣布 Swift 2 时,

第一章.面向对象与面向协议编程

本书是关于面向协议编程。当苹果2015年的开发者大会上发布了Swift2,他们也宣布Swift是第一种面向协议编程的语言。通过它的名字,我们可能会以为面向协议编程都是关于协议。并不是这样。这是一个错误的猜想。这是一个不仅是关于写应用,更是一个思考编程的方法。
在这章中,你将会学习:

  • Swift作为面向对象编程语言该如何使用
  • Swift作为面向协议编程语言该如何使用
  • 面向对象编程与面向协议编程的区别
  • 相比于面向对象编程,面向协议编程所提供的优势
    本书是关于面向协议编程,我们将会从讨论Swift如何被作为面向对象编程语言来开始。理解面向对象编程将会帮助我们理解面向协议编程,并且洞悉一些面向协议编程被设计用来解决的一些问题。

他们也声明 Swift 是世界上第一个面向协议编程的语言。通过它的名字, 我们可能认为面向协议编程都是跟协议相关的; 然而, 这可能是一个错误的假定。面向协议编程不仅仅是关于协议; 实际上它不仅是编写程序的新方式, 也是我们思考编程的新方式。

Swift是一门面向对象编程的语言

面向对象编程是一种设计哲学。使用面向对象编程的语言而不是面向过程的语言(比如C和Pascal)来写APP是完全不同的。面向过程的语言通过依赖程序一步步的告诉电脑怎么做。这可能看起来像一个给了明显名字的声明。但是最基本的,当我偶们考虑面向对象编程的时候,我们需要考虑到对象
该对象是一种数据结构,其以属性的形式包含关于对象的属性的信息,并且以方法的形式由对象执行或对对象执行的操作。对象可以被考虑成一种东西,在英语中,他们很正常的被考虑成介词。这些对象可以是真是世界或者虚拟对象。如果你环视四周,你将会看到很多真实世界的对象,并且在那里,它们都可以使用属性与操作以面向对象的方式来建模。
当我哦在写这章的时候,我看着窗外,并且看到一个湖,很多树,草地,我的狗和我院子里的栅栏。所有这些东西都可以使用属性与操作来建模成一个对象。
当我写这篇文章的时候,我也在思考一种我一直喜欢的运动饮料。这种能量饮料叫:Jolt。我不确定还有多少人记得Jolt苏打后者Jolt能量饮料,但是没有它们,我甚至都不能从学院毕业。一罐Jolt可以建模成为一个带有属性(净含量,咖啡因含量,温度和大小)和操作(喝和温度改变)。
我们可以把一罐Jolt放到一个Cooler的地方来给它降温。这个Cooler也可以被建模成一个对象,因为它有属性(温度,一罐Jolt,可以放的最大罐数)和操作(添加和移走Jolt)。
对象使得面向对象编程如此强大。使用对象,我们可以对真实世界的对象建模,比如一罐Jolt,我们也可以对虚拟世界里的对象建模,比如在电子游戏里的角色。这些对象可以在我们的应用里互动来构建真实世界的行为,或者在我们的虚拟世界里我们想要的行为。
在一个电脑应用内,我们不能在没有蓝图的情况下创建一个对象,这个蓝图告诉应用这个对象将会有什么属性和操作。在大多数的面向对象语言,这个蓝图以类的形式出现。一个类被构造用以允许我们来把对象的属性和操作封装成单一类型,该类型对我们试图代表的对象进行建模.
我们在我们的类中使用初始化器来创建类的实例。我们一般使用这些初始化器来为我们的对象初始化一类属性的值,或者执行其他类需要的初始化工作。一旦我们创建了一个类的实例,我们可以在我们的代理里使用它。
所有这些面向对象编程的解释都是好的,但是没有比真实的代码更好的演示观念的方法了。让我们看看我们怎么能够使用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(罐头本身的大小)。之后我们定义了一个初始化器,当我们创建类实例的时候,初始化器将会对对象的属性做初始化。最后,我们为罐子定义了两个操作。这两个操作是drinking(某人喝的时候会调用)。和temperatureChange(罐身温度改变的时候会调用)。
现在,让我们看看我们怎么对一个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现在的温度),cansOfJolt(Cooler里Jolt的罐数),maxCans(Cooler存放的最大罐数)。当我们创建Cooler类实例的时候,我们使用初始化器来初始化属性。最后,我们我们为Cooler定义了两个操作:addJolt用来给Cooler添加Jolt,removeJolt用来从Cooler中移除Jolt。现在,我们拥有了我们的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)")

在这个例子里,我们通过初始化器创建了一个Cooler的实例,并且设置了默认的属性。然后通过使用for-in循环创建了6个Jolt实例并加到了cooler实例中。最后,我们从cooler中取出一罐jolt,并且喝了一些。一杯清凉的jolt和jolt的咖啡因。还有比这更好的吗?
这个设计对我们简单的例子来说似乎很够用。然而,它真是不灵活。虽然我真是很喜欢咖啡因,但是我的妻子不喜欢。她更喜欢Caffeine Free Diet Coke(无咖啡因健怡可乐)。在现有的cooler设计下,当她往Cooler添加一些Caffeine Free Diet Coke的时候,我们会告诉她那是不可能的,因为我们的Cooler只能接受Jolt。这很不好,因为正不是真实世界里cooler的工作方式。而且我不想告诉我的妻子她不能存他的Diet Coke。(相信我,没人会想告诉她她不能存她的Diet Coke)。所以,我们如何使这个设计更加灵活?
这个问题的答案是polymorphism(多态性).polymorphism来自希腊单词Poly和Morph。在软件中,当我们想要在代码里使用单一接口来展现多种类型的时候,我们会使用多态性。多态给了我们使用统一格式来与多种类型作用的能力。当我们使用统一的接口与不同的对象交互的时候,我们能够随时添加符合接口的额外对象。我们可以在我们的代码里使用这些额外的类型,仅仅只需要一点甚至没有改变。
使用面向对象语言,我们可以使用多态,并且可以使用子类化来重用代码。子类化就是某一个类从另一个父类派生出一个子类。例如,我们有一个从人建模出来Person类,我们可以从Person类子类化出Student类。Student类会继承Person类的所有属性和方法。Student类可以重写它继承的任何属性和方法,也可以添加他自己额外的属性和方法。我们也可以添加其他的派生于Person类的类,并且我们可以使用Person类的接口来给这些所有子类做交互。
当一个类从另一个类派生出来,原始的类,被称为超类或父类,而新类被称为子类或者。在我们的person-student例子里,Person就是超类或父类,Student就是子类。在这本书里,我们会使用父类和子类。
多态可以使用子类化来实现,因为我们可以通过父类的接口来给所有的子类实例提供交互。举个例子,我们有三个子类(Student,ProgrammerFireman)都继承自Person类。那么我们可以使用Person类提供的接口来对三个子类提供交互。如果Person类提供了一个方法running(),那么我们可以确定,所有Person的子类都有一个方法叫running()(可能是来自父类的方法,也可能是来自Person类而被重写过的)。因此,我们可以在所有子类里使用running()方法。
让我们看看多态如何帮助我们添加Jolt以外的饮料到cooler里。在我们的原始例子里,由于Jolt以24盎司罐大小售卖,我们在对Jolt固定的罐头的大小。(soda有不同的尺寸,但是能量饮料只卖24盎司的)。下面的枚举器定义了我们的cooler可以接受的罐头尺寸:

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

DrinkSize枚举器让我们可以在cooler里放置12,16,24和32盎司大小罐头.
现在,让我们看看我们我们所有饮料将要派生的基类。我们会把这个基类命名为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现在被定义为DrinkSize类型而不是Double。我们给Drink类定义了一个初始化方法,这个初始化方法会初始化类的所有属性。最后,我们有两个与我们在Jolt类里一样的两个方法drinking()temperatureChange()。有一点需要注意的是,在Drink类里,我们的描述是社会成Drink base class
现在,让我们创建一个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罐头需要的温度。其他的值只需要设置成默认值就可以了。
现在,让我们看看如何创建一个可以接受除了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类的参数来代替。由于Jolt类是Drink类的子类,我们可以在所有 Drink类需要的地方使用它。接下来举个例子。下面的代码会创建一个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类的实例。这就是多态。既然我们有一个有Jolt的cooler,我们准备继续这次旅行。我妻子也想要把她的Caffeine Free Diet Coke放进去来冷藏。
我们不想剥夺她的Diet Coke,我们快速创建了我们可以使用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)
}

在这个例子里,我们创建了一个DrinkCooler的实例。我们放入了6罐Jolt和6罐Caffeine Free Diet Coke。使用多态,就像这里展示的这样。允许我们创建如我们所需数量的Drink类的子类。并且,我们可以在不改变Cooler代码的前提下,把他们添加到cooler里。这使得我们的代码变得十分灵活。
那么,当我们从cooler里拿出一罐饮料的时候会发生什么?很明显,当我妻子从中拿出一罐Jolt的时候,她会想要把它放回去,并且拿一罐不同的。但是,她是否知道应该拿哪一罐?
为了检查某个实例是否是需要的类型,我们使用类型检查方法is。如果实例类型是对的,is会返回true,反之亦然。在下面的代码里,我们使用is来持续从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循环持续循环直到foundCan的值被设成true。在while循环内,我们从cooler里移除饮料,然后使用is方法来看移除的实例是不是'Caffeine Free Diet Coke类的实例。如果是,那我们就把foundCan设置成true,然后设置wifeDrink变量设置成我们刚从cooler里移除的饮料。如果这个饮料不是Caffeine Free Coke Class类的实例,那我们会把饮料放回去,让循环返回到拿另一罐饮料。 在之前的例子里,我们展示了Swift如何被用作面向对象编程的语言。我们也是用了多态来让我们的代码灵活并且易扩展。然而,这种设计也有一些缺点。在我们转向面向协议编程之前。让我们其中两个缺点。然后,我们会看到面向协议编程如何使得这种设计更好。 第一个缺点是,我们对于饮料(Jolt,'Caffeine Free Diet Cokediet Coke)初始化方法的设计。当我们初始化一个子类的时候,我们需要调用父类的初始化方法。这是一把双刃剑。当调用父类的初始化方法的时候,它会给他们一致的初始化,但是如果我们不注意,她也会给我们不恰当的初始化。例如,我们使用如下代码创建另一个叫做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,而这不是我们想要的
我们需要注意,当我们像这样创建子类的时候,需要保证书友的属性被正确的设置。我们不能保证,父类的初始化方法会为我们设置好所有的属性。
这个设计的第二个缺陷是,我们使用引用类型。熟悉面向对象编程的人可能不会将其视为缺陷,并且他们在很多情况下喜欢使用引用类型。在我们的设计中,把饮料类型定义为值类型会更有意义。如果你对引用类型与值类型的工作原理不清楚,我们会在第二章的** 我们的类型选择 **中看到他们。
当我们通过一个引用类型的实例(就是说,我们传递给集合中的函数或集合,如数组),我们给原始的实例传一个引用。当我们传递一个值类型的实例,我们传递的是原始对象的新的拷贝。通过试验一下代码,让我们看看如果不注意的话,使用引用类型会引发什么问题:

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

在这个例子里,我们创建了一个包含Drink或者Drink子类的实例。然后我们创建了一个Jolt类,然后使用循环,6次加入到数组中。下一步,我们从数组里拿出第一罐,喝了一口,然后看看数组里所有的剩余容量。这段代码的结果如下:

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

可以看到,所有数组里的罐子里剩下的Jolt的余量都是相同的。这是因为我们创建了单一的Jolt实例,然后加入到jolts数组,我们给这个单一实例添加了6个引用。因此,当我们从数组里拿出第一罐,喝了一口,我们实际上是喝了数组里所有饮料。
对于有面向对象经验的程序员来说,这样的错误似乎不是问题。然而,他对初级开发者或者对不熟悉面向对象编程的开发者来说是很头大的。这些问题更多出现在有复杂初始化方法的类。我们可以通过在第六章的** 在Swift里采用设计模式 **提到的生成器模式避免这个问题,或者在我们的类中实现copy方法可以创建一个实例的拷贝。
另一个面向对象编程和子类化需要注意的地方是,如同前面例子显示的那样,一个类只能有一个父类。例如,DrinkJolt类的父类是Drink。这将会导致一个父类变得臃肿,其代码并不是所有子类 所需要或者想要的。这在游戏开发中是非常普遍的问题。
现在,让我们看看如何使用面向协议编程实现drink和cooler的例子。

在这一章, 你会学到:

Swift 作为面向协议编程

对于面向对象编程,我们经常从思考对象与类的继承来开始我们的设计。面向协议编程有点不同。这里,我们从协议开始思考设计。然而,在我们这个章节开始前,面向协议编程绝不仅仅只是协议。
通过这个部分,我们会就目前的例子,简要讨论组成面向协议编程的要素。我们会扎起下一个章节里深度讨论其中的要素,它能让你更好的理解如何在我们的应用了使用面向协议编程。
在之前的部分,我们把Swift看成一个面向对象编程的语言。我们使用类继承的的方式来设计架构,如下图所示:

金沙澳门官网网址 1

继承示例

为了使用面向协议编程重新设计它,我们需要重新思考这个设计的许多地方。第一个我们需要重新思考的是Drink类。面向协议编程规定我们应该从一个协议二不是父类开始。这意味着我们的Drink类应当编程一个Drink协议。我们将会使用这个协议的扩展来给遵守这个协议的饮料类型添加公有的代码。我们将在第四章中的** 所有关于协议的内容 讨论协议,我们将在第五章中的 让我们扩展一些类型 ** 介绍协议扩展。
第二个我们需要重新思考的地方是使用引用类型。苹果表示,在恰当的情况下,最好使用值类型而不是引用类型。当我们决定使用值类型还是引用类型的时候有很多需要考虑的地方,我们将会在第二章的** 我们的类型选择 **里讨论这个问题。在这个例子里,我们将会使用将会把我们的drink类型设置为值类型(structure),并把Cooler设置成引用类型。
在这个例子里,把drink类型设置成值类型,把Cooler设置成引用类型是基于我们将会如何使用这些类型的实例。drink类型的实例只有一个拥有者。例如,当drink在cooler里,cooler拥有它。当一个人把drink拿出来,drink就从cooler里移除,并且就它就属于拿了它的人。
Cooler类型与drink类型有点不同。drink类型一次只有一个拥有者与其作用。Cooler类型的实例则可能同时有几个部分与其发生作用。例如,当我们代码的某一部分往往cooler里添加drink的时候,有几个人的实例正从cooler里拿走饮料。
总结就是,我们之所以把drink类型设置成值类型,是因为,任何时候,我们的代码里只有一个拥有者可以与drink实例发生交互。然而,Cooler类型的对象,可以同时与我们代码的几个部分发生交互,所以它的类型设置成引用类型。
以下部分我们会在这本书里强调多次:引用类型与值类型的一个主要不同在于,我们如何传递这种类型的实例。当我们传递一个引用类型的实例。我们传递的是着原实例的引用。这意味着,实例的改变会反应在两个引用上。而当我们传递一个值类型的实例,我们传递的是原实例的新拷贝。这意味着,对一个实例的改变不会影响到另一个。
在我们进一步检验面向协议编程之前,我们先看看我们如何使用面向协议编程来重写我们的例子。我们将会从创建Drink协议开始:

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协议里,我们定义了5个属性,所有遵循了这个协议的类型都应该提供它们。DrinkSize的类型与之前在面向对象编程中的 DrinkSize一致。
在添加任何遵循我们的Drink协议的类型之前,我们想要拓展这个协议。协议拓展在Swift 2当中被加入,它允许我们给遵循协议的类型添加功能。这允许我们给所有遵循协议的类型定义行为而不是给它们添加行为。在我们的Drink协议的扩展里,我们定义两个方法:drinking()temperaturechange()。这与我们之前在面向对象编程中的Drink父类里定义的方法一样。以下是我们在Drink拓展里的代码:

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

现在,所有遵循Drink写一点类型都会自动接收到drinking()temperatureChange()方法。协议扩展对于遵循这个协议的类型来说,是完美的添加通用功能的方式。这与在父类当中添加功能来让所有子类接收到这个功能的方式相似。符合协议的单独类型也可以影响类似于超类的覆盖功能的扩展提供的任何功能。
现在,让我们创建JoltCaffeineFreeDietCoke类:

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

如我们所见,JoltCaffeineFreeDietCoke的类型是structure而不是class。这意味着,就像他们在面向对象设计中的一样,他们都是值类型而不是引用类型。这两种类型都在实现了我们在Drink协议中定义的属性以及将用于初始化的初始化方法。
相比面向对象例子里的drink类型,我们还需要在这些类型里加入一些别的代码。然而,我们很容易就能理解在饮料类型里发生了什么,因为所有东西都是在类型的本身初始化的,而不是在它的父类。最后,让我们看看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类与本章之前在面向对象编程中创建的类一样。把Cooler类创建为structure而不是一个类是可行的,但是它主要还是取决于我们打算如何在代码里使用它。早前,我们说过,我们代码的多个部分需要与cooler的一个实例交互。因此,在我们的例子里,把cooler作为引用类型比值类型要更好。

  • Swift 是怎么用作面向对象的编程的。
  • Swift 是怎么用作面向协议的编程语言的。
  • 面向对象编程和面向协议编程的区别。
  • 面向协议编程相对于面向对象编程的优点。
注意

苹果的建议是在适当的情况下优先考虑参考类型的价值类型。因此,在有所疑惑的时候,建议我们优先使用值类型而不是引用类型。
下图显示了新的设计:

金沙澳门官网网址 2

面向协议编程设计

现在,我们已经完成了重新设计,让我们总结下什么是面向协议编程以及它与面向对象编程的不同。

虽然这本书是关于面向协议编程的,我们将通过讨论 Swift 是怎么用作面向对象的编程语言开始。理解好面向对象的编程会有助于理解面向协议的编程并一窥面向协议编程设计所解决的问题。

总结面向协议编程与面向对象编程

我们刚刚看了Swift如何被用作面向对象编程语言和面向协议编程语言以及两者之间的不同。在本章的例子中,这两种设计的主要不同有两点。
我们在面向协议编程中看到的第一点不同是,我们应该以协议而不是父类开始。我们可以使用协议扩展来给遵循协议的类型添加功能。使用面向对象编程,我们使用父类开始。当我们重新设计我们的例子的时候,我们把Drink父类转化成Drink协议,然后使用协议扩展添加了drinking()temperatureChange()方法。
我们看到的第二个不同是,我们使用把drink类型定义成值类型(structures)而不是引用类型(class)。苹果说过,在恰当的情况下,应该使用值类型而不是引用类型。在我们的例子里,当我们事先drink类型的时候,使用值类型更合适。当然,我们仍然把Cooler定义成引用类型。
混合、匹配值类型与引用类型可能不是最好的长期维护代码的方法。我们在例子里使用它是为了强调值类型与引用类型的不同。在第二章的我们的类型选择里会详细讨论这点。
面向对象设计与面向协议设计都是用了多态来让我们使用相同的接口与不同的类型交互。使用面向对象设计,我们使用父类提供的接口来与所有子类交互。在面向协议的设计里,我们使用协议和协议扩展提供的接口来与遵守协议的类型交互。
现在,我们总结了面向对象编程设计与面向协议编程设计的不同,让我们再进一步看看这些不同。

作为面向对象编程语言的 Swift

面向对象编程是一种设计哲学。使用面向对象的编程语言写程序和使用老旧的诸如 C 和 Pascal 等过程式编程语言编写程序从根本上是不同的。过程式语言通过依靠过程使用一系列说明来告诉计算机每一步该怎么做。然而, 面向对象的编程全部是关于对象的。这似乎是一个非常明显的声明。但是本质上,当我们谈论面向对象编程的时候,我们需要考虑对象。

对象是包含属性和方法的数据结构。对象可以是一个东西,在英语中,它们通常被当做名词。这些对象可以是真实世界中的对象或者虚拟的对象。如果你四处看看,你会发现很多真实世界中的对象,并且,实际上,它们中的所有一切都能以一种带有属性和动作的面向对象的方式被模型化。

当我开始写这一章时,我看着外面,我看到了湖泊、很多树、草地、我的狗、还有我家后院中的篱笆。所有这些物品都可以被模型化为含有属性和动作的对象。

当我写这一章的时候,我也想起了我最喜欢的能量饮料。那种能量饮料叫做 Jolt。 不知道还有多少人记得 Jolt 碳酸水或 Jolt 能量饮料,但是我整个大学期间都在喝它。一罐加多宝可以被模型化为带有属性(容量、咖啡因量、温度和尺寸)和动作的对象。

我们可以把加多宝保存在冷藏器中来使它们保持冷却。这个冷藏器也可以被模型化为对象因为它拥有属性(温度、加多宝罐数、最大存储罐数)和动作。

正是对象让面向对象编程那样强大。使用对象,我们可以模型化真实世界中的对象, 例如加多宝的罐子、或视频游戏中的诸如字符的虚拟对象。这些对象之后可以在我们的应用程序中交互以模型化真实世界中的行为或我们想要的虚拟世界中的行为。

在计算机程序中,我们不能在没有能告知程序期望什么样的属性和动作的蓝图的情况下创建对象。在大部分面向对象的编程语言中,这个蓝图以类的形式出现。类是一种允许我们把对象的属性和动作封装到单个类型中的结构。

我们在类中使用构造函数(initializers)来创建类的实例。我们通常使用这些构造函数来为对象设置属性的初始值或执行我们的类需要的其它初始化。一旦我们创建了类的实例,之后就能在代码中使用它了。

关于面向对象编程的所有解释都很好,但是没有什么比实际的代码更能解释这个概念了。我们来看看在 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,description,和 canSize。然后我们定义了一个用于初始化对象属性的构造函数。该构造函数会确保所有的属性在实例被创建后都被合适地初始化了。最后,我们为 can 定义了两个动作。这两个动作是 driking(有人喝罐子中的饮料时调用)和 temperatureChange(罐子的温度变化时调用)。

现在,我们看看怎么模型化一个冷藏器以使我们的加多宝罐子保持冷藏,因为没有人喜欢加热的加多宝罐子:

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 return true } else { return false } } func removeJolt() -> Jolt? { if cansOfJolt.count > 0 { return cansOfJolt.removeFirst() } else { return nil } }}

我们以和模型化加多宝罐类似的方法模型化了冷藏器。我们以定义 3 个冷藏器的属性开始。那三个属性是 temperature(冷藏器中的当前温度)、cansOfJolt(冷藏器中加多宝的罐数)和 maxCans(冷藏器能装下地最大罐数)。当我们创建 Cooler 类的实例时,我们使用一个构造函数来初始化属性。最后,我们为冷藏器定义了两个动作。它们是 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}let jolt = cooler.removeJolt()jolt?.drinkingprint("罐子中还剩 (jolt?.volume) ml 加多宝")

在这个例子中,我们使用构造函数来设置默认属性创建了 Cooler 类的一个实例。当我们创建了 Jolt 类的 6 个实例并使用 for-in 循环把它们添加到冷藏器中。最后,我们从冷藏器中拿出一罐加多宝并喝了一点。还有比这更爽的吗?

这种设计对于我们这个简单地例子似乎工作的很好; 然而,它真的不是那么灵活。虽然我真的喜欢咖啡因,但是我妻子不喜欢;她更喜欢不含咖啡因的减肥可乐。如果使用我们当前的设计,当她要在冷藏器中添加一些她的减肥可乐时,我们不得不告诉她那是不可能的因为我们的冷藏器只接受 Jolt 实例。这不好,因为现实中冷藏器不是这么工作的,因为我不会告诉妻子她不能把减肥可乐放进冷藏器中(相信我没有人会愿意告诉他的妻子她不能把减肥可乐放进冷藏器中,不然等着看吧,她一整天都会唧唧歪歪的!) 所以,我们怎么使这个设计更加灵活呢?

答案是多态。多态来源于希腊单词 Poly和 Morph。在计算机科学中,当我们想在代码中使用单个接口来表示多个类型时我们使用多态。多态让我们拥有了以唯一的方式和多个类型进行交互的能力, 我们能在任何时候添加遵守那个接口的额外的对象类型。然后我们可以在代码中几乎不做更改地使用这些额外的类型。

使用面向对象编程语言,我们能使用子类化来达到多态和代码复用。子类化就是一个类继承自它的父类。例如, 假如我们有一个模型化普通人的 Person 类, 然后我们可以子类化 Person 类来创建 Student 类。则 Student 类会继承 Person 类的所有属性和方法。 Student 类可以覆盖任何它继承到的属性和方法并且/或者添加它自己的额外的属性和方法。之后我们能添加派生于 Person 超类的额外的类,并且我们能通过 Person 类呈现的接口来跟所有这些子类进行交互。

当一个类派生自另一个类,那么原来的类,即我们派生新类的那个类,就是人们熟知的超类或父类,派生出的新类就是人们所熟知的孩子或子类。在我们的 person-student 例子中, Person 类是超类或父类, 而 Student 是子类或孩子类。 在这本书中,我们会一直使用属于超类和子类。

多态是使用子类化达到的因为我们能通过超类呈现的接口和所有子类的实例进行交互。举个例子,如果我们有 3 个孩子类(StudentProgrammer,和 Fireman)都是 Person 类的子类,那么我们能够通过 Person 类呈现的接口跟所有 3 个子类进行交互。如果 Person 类拥有一个名为 running() 的方法,那么我们可以假定 Person 类的所有子类都拥有一个名字 running()的方法(这个方法要么继承自 Person 要么是子类中的方法覆盖了 Person 类的中的方法)。因此,我们可以使用 running() 方法跟所有子类进行交互。

让我们来看看多态是怎么帮助我们把 drinks 而非 Jolt 添加到我们的冷藏器中的。在我们原来的例子中,我们可以在 Jolt 类中硬编码罐子的尺寸因为 Jolt 能量饮料只有 24 盎司的罐子卖(苏打有不同的尺寸,但是能量饮料只卖 24 盎司的)。下面的枚举定义了冷藏器将接受的罐子尺寸:

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

这个 DrinkSize 枚举让我们在冷藏器中使用 12、16、24 和 32 盎司的饮料尺寸。

现在, 让我们看看我们的基类或超类,所有的 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 现在被定义为 DrinkSize 类型而非 Double 类型。我们为 Drink 类型定义了单个构造函数以初始化类中的所有 5 个属性。最后, 我们拥有了和原来的 Jolt 类相同的两个方法,它们是 drinking()temperatureChange()。需要注意的一件事情是,在 Drink 类中,我们的 description 属性被设置到 Drink 基类中了。

现在,我们创建一个 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 类添加了一个构造函数。这个构造函数要求提供 Jolt 罐子的温度。所有其他的值被设置为它们给 Jolt 罐提供的默认值。

现在, 我们看看怎么创建 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 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 实例的任何地方。我们来看看这是怎么工作的。下面的代码会创建一个 Cooler 类。然后向冷藏器中添加 6 罐加多宝,再从冷藏器中拿出一罐加多宝来喝:

var cooler = Cooler(temperature: 38.0, maxCans: 24)for _ in 0...5 { let can = Jolt(temperature: 45.1) let _ = cooler.addDrink}let jolt = cooler.removeDrink()cooler.cansOfDrinks.countjolt?.drinkingprint("这罐加多宝还剩 (jolt?.volume) 盎司")

注意在这个例子中, 在需要 Drink 类实例的地方我们使用了 Jolt 类的实例。这就是多态。既然我们冷藏器中有了 Jolt,我们将要开始我们的旅途了。我妻子当然想带上她的不含咖啡因的减肥可乐,所以她问我能否在冷藏器中放上一些以保持冷藏。我知道我们阻止不了她喝减肥可乐,所以我们很快创建了一个可以使用的 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 类的子类,并且它们都定义了一个构造函数来初始化类。关键是它们都是 Drink 类的子类,这意味着在冷藏器中这两个类的实例我们都可以使用。因此, 当我的妻子带了 6 瓶不含咖啡因的减肥可乐时,我们可以把它们放进冷藏器中就像我们存放加多宝罐子一样。下面的代码解释了这个:

var cooler = Cooler(temperature: 38.0, maxCans: 24)for _ in 0...5 { let can = Jolt(temperature: 45.1) let _ = cooler.addDrink;}for _ in 0...5 { let can = CaffeineFreeDietCoke(volume: 15.5, temperature: 45, drinkSize: DrinkSize.Can16) let _ = cooler.addDrink}

在这个例子中, 我们创建了一个冷藏器实例;我们在冷藏器中放进了 6 罐加多宝和 6 罐不含咖啡因的减肥可乐。使用多态,就像这儿展示的,允许我们创建尽可能多的 Drink 子类, 并且所有这些子类都可以用在 Cooler 类中而不需更改 Cooler 类的代码。这让我们的代码更加灵活。

所以,当我们从冷藏器中拿走一个罐子会发生什么? 显然地,如果我妻子拿到一罐加多宝,她会把它放回去并拿走另外不同的一罐。但是她怎么知道她拿到的是什么呢?

为了检查某个实例是哪个特定的类型,我们使用类型检查操作符。如果实例的类型是那个类型则类型检查操作符返回 true, 否则就返回 false。在下面的代码中,我们使用类型检查操作符来不断地从冷藏器中移除罐子直到我们找到不含咖啡因的减肥可乐为止:

var foundCan = falsevar wifeDrink: Drink?while !foundCan { if let can = cooler.removeDrink() { if can is CaffeineFreeDietCoke { foundCan = true wifeDrink = can } else { cooler.addDrink } }}if let drink = wifeDrink { print("拿到了 " + drink.description)}

在这个代码中,我们的 while 循环持续循环直到 foundCan 布尔值被设置为 true。 在 while 循环中,我们从冷藏器中拿出一罐饮料然后使用类型检查操作符来看我们拿出的罐子是否是 CaffeineFreeDietCoke 类的实例。如果它是 CaffeineFreeDietCoke 类的实例,那么我会把 foundCan 布尔值设置为 true 并把 wifeDrink 变量设置为刚从冷藏器中拿走的那罐饮料的实例。 如果那罐饮料不是 CaffeineFreeDietCoke 类的实例,那么我们会把罐子放回到冷藏器中并回到循环中以抓取另外一罐饮料。

在上面的例子中, 我们展示了 Swift 是怎么用作面向对象的编程语言的。我们还使用了多态来让我们的代码更加灵活并且更容易扩展;然而,这个设计有几个缺点。在我们进行面向协议编程之前,我们来看看这两个缺点。然后,我们会看到面向协议编程是怎么让这个设计更好的。

我们的设计中的第一个缺点是 drink类(Jolt, CaffeineFreeDietCoke 和 DietCoke) 的初始化。当我们初始化子类的时候,我们需要调用超类的构造函数。这是把双刃剑。虽然调用我们超类的构造函数让我们拥有了一致性的初始化,但是如果我们不小心的话,它也会带给我们不合适的初始化。例如,假设说我们使用如下代码创建了另外一个叫做 DietCokeDrink 类:

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

如果我们仔细看, 我们会看到在 DietCoke 类中的构造函数中, 我们根本没有设置 description 属性。因此,这个类的 description 会使用 Drink 基类中的 description, 这不是我们想要的。

当我们创建这样的子类的时候要小心以确保所有的属性被合理的设置了, 我们不能指望超类的构造函数会为我们合理地设置所有的属性。

我们的设计的第二个缺点是我们使用了引用类型。 虽然那些熟悉面向对象编程的人可能认为这不是一个缺点并且很多情况下都偏好使用引用类型,在我们的设计中,把 drink 的类型定义为值类型更有意义。如果你不熟悉引用类型和值类型是如何工作的,我们会在第二章,我们的类型选择里深入讨论它们。

当我们传递引用类型(即我们传递给函数或像数组那样的集合)时,我们传递的时对原实例的引用。当我们传递值类型的实例时,我们传递的是对原实例的一份拷贝。通过实验下面的代码,我们来看看如果我们不小心地使用了引用类型会导致什么问题:

var jolts = [Drink]()var myJolt = Jolt(temperature: 48)for _ in 0...5 { jolts.append}jolts[0].drinkingfor (index, can) in jolts.enumerate() { print("Can  amount Left: (can.volume)")}

在这个例子中,我们创建了一个会包含 Drink 类或 Drink 类的子类的实例的数组。之后我们创建了一个 Jolt 类的实例并在数组里放了 6 罐加多宝。接着,我们从数组中拿出第一罐来喝并打印出数组中每罐加多宝的剩余容量。如果我们运行这段代码,我们会看到如下结果:

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

就像我们从结果中看到的,数组中的所有加多宝罐子都拥有同样的剩余容量。这是因为我们创建了 Jolt 类的单个实例,之后我们在 jolts 数组中添加了 6 个该单个实例的引用。因此,我们从数组中拿出第一罐饮料时来喝时,我们实际上把数组中的每一罐都拿出来喝了。

这种错误对于有经验的面向对象的程序员看起来不是什么问题;然而,它经常出现在不熟悉面向对象编程的初级程序员或开发者之中很令人吃惊。当类的构造函数很复杂时这种错误出现的更加频繁。我们可以通过使用第六章,在 Swift 中遵循设计模式中看到的 Builder 模式来避免这个问题,或者在我们的自定义类中实现一个 copy 方法以拷贝一份实例。

就像上面的例子中展示的那样,面向对象编程和子类化需要注意的另外一件事情是, 一个类只能拥有一个超类。例如,我们的 Jolt 类的超类是 Drink 类。这可能导致单个超类变得非常臃肿并且包含所有子类中所不需要或不想要的代码。这在游戏开发中是一个普遍的问题。

现在, 我们来看看怎么使用面向协议的编程来实现我们的 drinks 和 cooler 例子。

面向协议编程与面向协议编程

我在本章的开头提到过,面向协议编程绝不仅仅只包含协议。并且它不仅可以被用来写应用,更是思考编程的一种新方式。在这个部分,我们会测试两种设计的不同来看看上述陈述的真实意义。
作为一个开发中,我们的首要目标是开发一个好的app,但是我们也应该专注于写一个简介、安全的代码。在这个部分,我们会专注于讨论简介、安全的代码,所以让我们看看这两个词意味着什么。
简介的代码意味着容易阅读与理解。简介的代码是非常重要的,因为我们的写的任何代码需要被人保留下来,而这个人往往就是写代码的人。没有比往回看你自己的代码而不能理解它的时候。简洁、易理解的代码,也有助于更容易发现其中的错误。
安全的代码意味着很破坏它。没有比一下更让一个开发者苦恼的事了:当你在代码里做了一小点改变的时候,有不少错误出现在代码里或者应用里出现很多bug。写出简洁的代码,我们的代码能够被安全的继承,因为其他的开发者能够准确的理解它所表达的意思。
现在,让我们简要看看协议/协议扩展与父类的不同。我们会在第四章的关于协议的一切和第五章的让我们扩展一些类型里讨论更多相关内容。

作为面向协议编程语言的 Swift

使用面向对象编程,我们在设计时通常从考虑对象和类的层级开始。面向协议编程有点不同。这儿, 我们在设计时从考虑协议开始。然而,就像我们在这一章的开头所说的,面向协编程不仅仅是关于协议的。

当我们浏览这一节时, 我们会就我们当前的例子简要讨论下组成面向协议编程的不同条目。然后我们会在接下来的几章里深入探讨这些条目以让你更好地理解在我们的应用程序中是怎么完整地使用面向协议编程的。

在之前的小节中,当我们把 Swift 用作面向对象的编程语言时, 我们使用类的层级来设计我们的方案, 就像下面的展示图一样:

为了用面向协议编程重新设计这个方案,我们需要重新思考该设计的几个方面。第一个方面是我们怎么重新考虑 Drink 类。面向协议编程声明我们应该从协议而不是超类开始。这意味着我们的 Drink 类会变成 Drink 协议。我们可以使用协议扩展为遵守该协议的 drink 类添加通用代码。我们将在第四章关于协议中复习协议,并且我们会在第五章让我们扩展某些类型中涵盖协议扩展。

我们要重新思考的第二个方面是引用类型的使用。在 Swift 中,苹果已经声明了在尽可能合理的地方更偏好使用值类型胜过使用引用类型。是使用引用类型还是使用值类型有很多地方需要考虑,我们会在第二章中深入了解这个问题。在这个例子中,我们会在 drink类型(Jolt 和 CaffeineFreeDietCoke)中使用值类型,在 Cooler 类型中使用引用类型。

在这个例子中,为 drink 类型使用值类型和为 Cooler 类型使用引用类型的决定依赖于我们怎么使用这些类型的实例。我们的 drink 类型的实例只会有一个拥有者。例如,当饮料在冷藏器中时,冷藏器就拥有了它。但是之后,当有人把它从冷藏器中拿了出来,这个人就拥有了它。

Cooler 类型和 drink 类型有点不一样。虽然 drink 类型一次只会有一个拥有者和它交互,但是 Cooler 类型可能在代码中拥有几个部分来跟它进行交互。例如,我们可以让代码的一部分为冷藏器添加饮料而让几个人的实例从冷藏器中喝饮料。

总的来说, 我们使用值类型来模型化我们的 drink 类型因为一次只有代码的一部分能跟 drink 类型的实例进行交互。然而,我们使用引用类型来模型化 Cooler 因为我们的代码的多个部分将和 Cooler 类型的同一个实例进行交互。

我们会在该书中多次强调这点:引用类型和值类型的一个主要区别是我们怎么传递类型的实例。当我们传递引用类型的实例时, 我们传递的是对原实例的引用。这意味着变化被反射到这两个引用中。当我们传递值引用的时候,我们传递的是对原实例的一份拷贝。这意味着一个实例中的更改不会反射到其他实例中。

在我们你一步实验面向协议编程之前, 我们来看看怎么以面向协议编程的方式来重写我们的例子。我们将以创建 Drink 协议开始:

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 协议中, 我们定义了每个遵守该协议的类型必须要提供的 5 个属性。 DrinkSize 类型和我们这一章中面向对象小节中的 DrinkSize 一样。

在我们添加遵守 Drink 协议的任何类型之前,我们想扩展一下这个协议。协议扩展在 Swift 2 中被添加进来,它允许我们为遵守该协议的类型提供功能。这让我们为遵守该协议的所有类型定义行为,而不是把行为添加到每个遵守该协议的单独的类型中。在 Drink 协议的扩展中,我们会定义两个方法: drinking()temperatureChange()。 这个这一章中的面向对象编程中的 Drink 超类中的两个方法相同。下面是 Drink 扩展的代码:

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

现在,任何遵守 Drink 协议的类型都会自定地接收到 drinking() 方法和 temperatureChange 方法。协议扩展很适合为遵守协议的所有类型添加通用的代码。这和为超类添加功能很像,其中所有的子类从超类中接收功能。单独遵守协议的类型也能遮蔽由协议扩展所提供的功能,这和重写超类中的功能类似。

现在我们来创建 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 类型都是结构体而非类。这意味着它们都是值类型而非引用类型,就像它们在面向对象设计中一样。这两个类型都实现 Drink 协议中定义的 5 个属性还有一个构造函数用于初始化类型的实例。和面向对象的 drink 类的例子相比,这些类型需要的代码更多。然而,在这里很容易理解这些 drink 类型中发生了什么因为所有东西是在类型自身中初始化的,而非在它们的超类中初始化。

最后, 我们看看 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 return true } else { return false } } func removeDrink() -> Drink? { if cansOfDrinks.count > 0 { return cansOfDrinks.removeFirst() } else { return nil } }}

如我们所见, Cooler 类和我们在这一章的面向对象编程一节中所创建的类相同。对于把 Cooler 类型创建为结构体而非类会颇有微词, 但是它实际上取决于我们在代码中打算怎么用它。之前, 我们声明代码的多个部分会需要跟单个 cooler 实例进行交互。因此,最好把我们的 cooler 实现为引用类型而非值类型。

协议和协议扩展对比父类

在面向对象编程的例子里,我们创建了一个从所有drink类派生出的Drink父类。而在面向协议编程的例子里,我们结合协议与协议扩展来达到相同的结果;当然,使用协议有很多的优势。
为了强化之前对于两种结论的记忆,让我们看看Drink父类与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.description = "Drink base class"
        self.drinkSize = drinkSize
    }

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

}

Drink父类是我们创建实例的完整类型。这可能是好事,也可能是坏事。有时候,像这个例子,当我们不用创建父类的子类的时候;我们只需要创建子类的实例。这种时候,我们依然可以利用面向对象编程的协议。然而,我们还是需要使用协议扩展来添加公有的功能,这会让我们沿着面向协议编程的路走去。
现在,让我们看看我们如何使用面向协议编程来创建Drink协议和Drink协议扩展:

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

两种结论下的代码都很安全且好理解。作为个人参考,我喜欢把实现从定义里分离。因此,对我而言,协议/协议扩展的代码会更好,但这真的只是一个参考。然而,我们会在接下来的几页里看到协议/协议扩展作为一个一个整体来说,会是更清晰且更好理解的。
协议/协议扩展相比父类来说还有三个优势。第一个优势是,类型可以遵循多个协议;但是他们只能有一个父类。这就意味着,我们可以创建多个协议来添加指定功能而不是创建一个整体的父类。例如,对我们的Drink协议,我们也可以创建DietDrinkSodaDrinkEnergyDrink协议,它们包含这些饮料的指定需要和功能。然后,DietCokeCaffeineFreeDietCoke类型要遵循DrinkDietDrinkSodaDrink协议。而Jolt结构体会遵循DrinkEnergyDrink协议。而使用父类,我们需要把DietDrinkSodaDrinkEnergyDrink协议都的内容都定义到一个单一的整体父类中。
第二个优势是我们可以使用协议扩展添加功能,而不需要源代码。这意味着,我们可以扩展任意协议,即使这个协议是Swift语言本身的一部分。而为了给父类添加功能,我们需要源代码。我们可以使用扩展给父类添加功能,但那意味着所有子类将会继承这个功能。当然,一般情况下,我们使用扩展给指定的类添加扩展,而不是一个类的层次结构。
第三个优势是,协议/协议扩展可以被类、结构体、枚举所采用,但是类的继承被限制在类类型。协议/协议扩展给我们在恰当情况下使用值类型的选项。

本文由金沙澳门官网网址发布于电脑系统,转载请注明出处:金沙澳门官网网址面向对象编程和面向协议编程

关键词: