对于面向对象的编程,我们通常通过思考对象和类层次结构来开始设计。面向协议的编程有点不同。在这里,我们通过思考协议来开始我们的设计。然而,正如我们在本章开头所陈述的,面向协议的编程不仅仅是协议。
在本节中,我们将通过当前示例简要讨论的面向协议的编程的不同要素。然后,我们将在接下来的几章中深入讨论这些要素,让您更好地理解如何在应用程序中使用面向协议的编程。
在上一节中,当我们将 Swift
视为面向对象的编程语言时,我们使用类层次结构设计了解决方案,如下图所示:
要使用面向协议的编程方式重新设计此解决方案,我们需要重新考虑此设计的几个方面。首先重新思考 Drink
类。面向协议的编程指出,设计应该从协议开始而不是超类。这意味着我们的 Drink class
将成为一种 Drink protocol
,使用协议扩展添加遵循协议的饮料类型通用代码。我们将在第4章中讨论协议,所有关于协议的内容,我们将在第5章中讨论协议扩展。
其次重新思考引用 (类) 类型的使用,对于Swift
,苹果已经声明,在适当的情况下,最好使用值类型而不是引用类型。选择是使用引用类型还是值类型是需要很多要考虑的,我们将在第2章,深入讨论类型选择。在这个例子中,饮料类型 (Jolt和咖啡因焦糖) 使用值 类型,Cooler
类型使用引用类型。
在这个例子中,饮料类型使用值类型,冷却器类型使用参考类型,是基于我们将如何使用这些类型的实例。饮料类型只有一个主人。例如,当饮料在冷却器中时,冷却器拥有它。但是,当一个人把饮料拿出来时,饮料会从冷却器中取出,交给一个拥有它的人。
冷却器类型与饮料类型略有不同。虽然饮料类型一次与一个所有者交互,但冷却器类型的实例可能有几个部分的代码与之交互。例如,我们可能有一部分代码将饮料添加到冷却器中,而我们有几个人从冷却器中获取饮料的实例。
总而言之,我们使用值类型 (structure
) 来构造饮料类型,在任一时刻,应该只有一部分代码与饮料类型的实例交互。我们使用引用类型 (class
) 来构建冷却器,因为多个部分的代码将与冷却器类型的同一实例进行交互。
我们将在本书中多次强调这一点: 引用类型和值类型之间的主要区别之一是我们如何传递类型的实例。当我们传递引用类型的实例时,是传递一个的指向原始实例引用,意味着所做的更改在两个引用都会受影响。当我们传递值类型的实例时,是传递原始实例的新副本,意味着在一个实例中所做的更改不会在另一个实例中有所反映。
在进一步检查面向协议的编程之前,让我们看一下如何以面向协议的编程方式重写示例。我们将从创建我们的 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
协议中,我们定义了五个属性是每种类型必须遵循的。DrinkSize
类型与我上一节面向对象部分中定义的 DrinkSize
类型相同。
在添加遵循 Drink
协议的类型之前,我们想扩展协议。协议扩展在 Swift 2.0
支持的,它们允许为符合要求的类型提供功能。这使我们能够定义符合协议的所有类型的行为,而不是将行为添加到每个单独的符合协议的类型。在我们的 Drink
协议扩展中,我们将定义 2 个函数:drinking()
和 temperaturChange()
。这两个方法在面向对象编程里,会放到 Drink
超类里。以下是Drink
扩展的代码:
extension Drink {
mutating func drinking(amount: Double) {
volume -= amount
}
mutating func temperatureChange(change: Double) {
temperature += change
}
}
现在,任何遵循 Drink
协议的类型都将自动接收 drinking()
和 temperaturChange()
方法。协议扩展非常适合向遵循协议的所有类型添加通用功能。这类似于向超类添加功能,其中所有子类都从超类接收功能。符合协议的各个类型也可以对扩展提供的任何函数进行 shadow
,类似于从超类中覆盖函数。
现在让我们创建 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.drinkSize = DrinkSize.Can24
self.description = "Jolt Energy Jolt"
}
}
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.drinkSize = drinkSize
self.description = "Caffeine Free Diet Coke"
}
}
正如我们所看到的,Jolt
和 CaffeineFreeDietCoke
类型都是结构而不是类。意味着它们都是值类型,而不是引用类型,就像在面向对象设计中一样。这两种类型都实现了在 Drink
协议中定义的五个属性以及将用于初始化实例的构造函数。
与面向对象示例中的饮料类相比,这些类型需要更多的代码。然而,更容易理解这些饮料类型中发生了什么,因为所有属性都是在类型本身中初始化的,而不是在超类中。
最后,让我们看看冷却器类型:
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
类与在面向对象编程部分中创建的类相同。将冷却器类型创建为结构而不是类可能有一个非常可行的论点,但这实际上取决于我们计划如何在代码中使用它。之前,我们声明代码的各个部分将需要与冷却器的单个实例进行交互。因此,在我们的示例中,最好将我们的冷却器实现为引用类型而不是值类型。
Apple 的建议是在适当的情况下优先使用值类型而不是引用类型。因此,当有疑问时,建议我们使用值类型而不是引用类型。
下图显示了新设计的外观:
现在我们已经完成了重新设计,下一节总结一下什么是面向协议的编程,以及它与面向对象的编程有何不同。