第一章.面向对象与面向协议编程
本书是关于面向协议编程。当苹果2015年的开发者大会上发布了Swift2,他们也宣布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
,Programmer
和Fireman
)都继承自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 Coke
和diet 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看成一个面向对象编程的语言。我们使用类继承的的方式来设计架构,如下图所示:
为了使用面向协议编程重新设计它,我们需要重新思考这个设计的许多地方。第一个我们需要重新思考的是
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()
方法。协议扩展对于遵循这个协议的类型来说,是完美的添加通用功能的方式。这与在父类当中添加功能来让所有子类接收到这个功能的方式相似。符合协议的单独类型也可以影响类似于超类的覆盖功能的扩展提供的任何功能。
现在,让我们创建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。这意味着,就像他们在面向对象设计中的一样,他们都是值类型而不是引用类型。这两种类型都在实现了我们在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如何被用作面向对象编程语言和面向协议编程语言以及两者之间的不同。在本章的例子中,这两种设计的主要不同有两点。
我们在面向协议编程中看到的第一点不同是,我们应该以协议而不是父类开始。我们可以使用协议扩展来给遵循协议的类型添加功能。使用面向对象编程,我们使用父类开始。当我们重新设计我们的例子的时候,我们把Drink
父类转化成Drink
协议,然后使用协议扩展添加了drinking()
和 temperatureChange()
方法。
我们看到的第二个不同是,我们使用把drink类型定义成值类型(structures)而不是引用类型(class)。苹果说过,在恰当的情况下,应该使用值类型而不是引用类型。在我们的例子里,当我们事先drink类型的时候,使用值类型更合适。当然,我们仍然把Cooler
定义成引用类型。
混合、匹配值类型与引用类型可能不是最好的长期维护代码的方法。我们在例子里使用它是为了强调值类型与引用类型的不同。在第二章的我们的类型选择里会详细讨论这点。
面向对象设计与面向协议设计都是用了多态来让我们使用相同的接口与不同的类型交互。使用面向对象设计,我们使用父类提供的接口来与所有子类交互。在面向协议的设计里,我们使用协议和协议扩展提供的接口来与遵守协议的类型交互。
现在,我们总结了面向对象编程设计与面向协议编程设计的不同,让我们再进一步看看这些不同。
面向协议编程与面向协议编程
我在本章的开头提到过,面向协议编程绝不仅仅只包含协议。并且它不仅可以被用来写应用,更是思考编程的一种新方式。在这个部分,我们会测试两种设计的不同来看看上述陈述的真实意义。
作为一个开发中,我们的首要目标是开发一个好的app,但是我们也应该专注于写一个简介、安全的代码。在这个部分,我们会专注于讨论简介、安全的代码,所以让我们看看这两个词意味着什么。
简介的代码意味着容易阅读与理解。简介的代码是非常重要的,因为我们的写的任何代码需要被人保留下来,而这个人往往就是写代码的人。没有比往回看你自己的代码而不能理解它的时候。简洁、易理解的代码,也有助于更容易发现其中的错误。
安全的代码意味着很破坏它。没有比一下更让一个开发者苦恼的事了:当你在代码里做了一小点改变的时候,有不少错误出现在代码里或者应用里出现很多bug。写出简洁的代码,我们的代码能够被安全的继承,因为其他的开发者能够准确的理解它所表达的意思。
现在,让我们简要看看协议/协议扩展与父类的不同。我们会在第四章的关于协议的一切和第五章的让我们扩展一些类型里讨论更多相关内容。
协议和协议扩展对比父类
在面向对象编程的例子里,我们创建了一个从所有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
协议,我们也可以创建DietDrink
、SodaDrink
和EnergyDrink
协议,它们包含这些饮料的指定需要和功能。然后,DietCoke
和CaffeineFreeDietCoke
类型要遵循Drink
、DietDrink
和SodaDrink
协议。而Jolt
结构体会遵循Drink
和EnergyDrink
协议。而使用父类,我们需要把DietDrink
、SodaDrink
和EnergyDrink
协议都的内容都定义到一个单一的整体父类中。
第二个优势是我们可以使用协议扩展添加功能,而不需要源代码。这意味着,我们可以扩展任意协议,即使这个协议是Swift语言本身的一部分。而为了给父类添加功能,我们需要源代码。我们可以使用扩展给父类添加功能,但那意味着所有子类将会继承这个功能。当然,一般情况下,我们使用扩展给指定的类添加扩展,而不是一个类的层次结构。
第三个优势是,协议/协议扩展可以被类、结构体、枚举所采用,但是类的继承被限制在类类型。协议/协议扩展给我们在恰当情况下使用值类型的选项。
实现drink类型
drink类型的实现在面向对象与面向协议里的例子是有很大差别的。我们将会看看这两个例子的不通电。但是首先,我们需要再次惠顾代码来提醒我们如何实现drink类型。我们首先看看,我们在面向对象例子里如何实现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
父类的子类,他们的实现里面也都只有一个初始化方法。虽然这只是非常简单且直接的实现,我们还是需要完全理解父类被期望能够恰当的实现他们。例如,如果我们不充分理解Drink
父类,我们可能忘记设置description。在我们的例子里,忘记设置description可能不是大问题,但是在更复杂的类型了,忘记设置属性可能导致未曾预料的行为。我们可以通过在父类的初始化方法里设置所有属性来防止出现这些问题;当然,在有些情况下可能行不通。
现在,让我们看看我们如何在面向协议编程例子里实现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
}
}
在面向协议编程例子实现的类型比面向对象编程里的例子要重要的多。当然,面向协议编程例子里的代码更安全也更好理解。我们说面向协议的更安全更好理解的理由在于我们在两个例子里如何实现属性和初始化方法。
在面向对象编程例子里,所有的属性都在父类里定义。我们需要查看代码或者文档看父类定义了什么属性以及他们如何定义。而在协议里,我们也需要查看协议本身或者文档来看如何实现协议,但是这个实现是在类型本身实现的。这就允许我们能够看到这个类型的一切是怎么实现的,而不用查看父类的代码或者挖掘整个的类的继承关系来看事情是如何实现、初始化的。
子类的初始化方法也必须调用父类的初始化方法来保证父类的属性被恰当的设置。这能够保证在子类当中有一致的初始化。它也隐藏了类是如何初始化的。在协议例子里,所有的初始化工作都是在它自身完成的。因此,我们不需要查看整个类继承来看一切是如何初始化的。
Swift里的父类给我们的需求提供了实现。swift中的协议只是一个合同,表示符合给定协议的任何类型都必须满足协议规定的请求。因此,使用协议,所有的属性、方法和初始化方法都要在类型本身。这允许我们能够简单的看到所有东西是如何被定义和初始化的。
值类型vs引用类型
在引用与值类型之间有几点本质不同,并且我们将会在第二章的我们的类型选择里详细讨论。现在,我们将会把注意力集中在这两种类型的一个主要不同:类型如何传递。当我们传递一个引用类型的实例,我们把原始对象的引用传递过去。这意味着,任何的变动都会反应会原始的对象。当我们传递一个值类型的对象,我们传递的是原对象的新拷贝对象。这意味着我们做的改变都不会反应到原对象上。
如同我们之前提到的那样,在我们例子里,一个drink类型的实例一次只能有一个拥有者。不应该出现我们代码的多个部分同事与drink类型的实例交互。作为例子,当我们创建创建一个drink类型的实例的时候,我们会把它放入cooler类型的一个实例。然后,如果有个人过来,并把它从冰箱里移除,这个人将会拥有这个drink的实例。如果这个人把drink给了别人,那么第二个人就会拥有这个drink。
使用值类型保证我们始终能够拿到唯一的实例,因为我们传的是原实例的拷贝对象而不是它的引用对象。因此,我们相信我们代码的别的部分不会有对这个实例有意想不到的改变。这在多线程环境里尤其有用,因为不同线程可以修改数据,发生未可知的行为。
我们需要保证恰当的使用值类型与引用类型。在这个例子里,drink类型示范了值类型应该被优先考虑,而Coler类型示范了引用类型应当被优先考虑。
在多数面向对象语言里,我们没有把自定义类型设置成值类型的选项。在Swift里,类和结构体在功能上比其他语言更加接近。我们也能够把自定义类型设置成值类型。我们只需要保证创建自定义类型的时候,我们使用了恰当的类型就可以了。我们将会在第二章的我们的类型选择里再详细讨论这点。
胜者是...
当我们读过这章并且看到面向协议编程相对面向对象编程的优势,我们可能想到面向协议编程比面向对象编程更清晰。当然,这种猜想可能并不十分准确。
面向对象编程从1970年代就出现了,并且它是一个被试过的真正的编程范例。面向协议编程是另一方便的新事物并且它是被设计用来修正一些面向对象编程的一些问题。我个人在一些项目里使用了面向协议编程范例,并且我对它的可能性感到很兴奋。
面向对象编程和面向协议编程有相似的原理,比如从真实世界的事务里建模创建自定义类型并且通过多态来使用单一接口来与多种类型交互。区别在于原理的实现。
对我来说,项目里基于面向协议编程的代码比使用面向对象编程的更加易于阅读。这并不意味着我会停止使用面向对象编程。我仍然可以看到许多类的层次结构和继承的需要。
记住,当我们设计我们的应用的时候,我们应当总是在正确的地方使用正确的工具。我们不会想要用电锯去切一块2x4的木块,我们也不想要使用sklsaw(电锯牌子)来砍倒一棵树。因此,胜者是程序员,我们有使用不同编程范例的选择,而不是被限制在一种上。
总结
在这章里,我们见到了Swift如何被用作面向对象编程语言,�也看到了它如何被用作面向协议编程语言。虽然,这两种编程范例有相似的逻辑,但是他们的实现是不同的。
使用面向对象编程,当我们创建对象的时候,我们会使用类作为我们的蓝图。使用面向协议编程,我们有使用类、结构体、枚举的选择。我们甚至可以使用其他类型,就像我们将会在第二章的我们的类型选择里看到的。
使用面向对象编程,我们可以使用类继承实现来实现多态。使用面向协议编程,则使用结合协议和协议扩展的方式来实现多态。我们将会在第四章的关于协议的一切里深入了解协议。
使用面向对象编程,我们能够在我们的子类中实现由子类继承的功能。子类有能够重写来自父类的功能。使用面向协议编程,我们使用协议扩展来给遵循协议的类型添加功能。如果他们选择的话,这些类型可以影响这个功能。我们将会在第五章的让我们扩展一些类型里再深入探讨协议扩展。
面向对象编程从1970年代就出现了,并且它是一个被试过的真正的编程范例。它也开始显示出一些汗与泪水。在这章里,我们看到了其中的问题和面向协议编程被设计用来解决的设计问题。
现在,我们已经看过了面向协议编程的概述,是时候看看组成面向协议编程各个部分的详情了。通过更好的理解这些不同的地方,我们能够更好的在应用里实现面向协议编程。我们将会看到Swift语言提供的各种类型以及我们如何使用他们。