POP-01.2-面向协议编程

75 阅读6分钟

对于面向对象的编程,我们通常通过思考对象和类层次结构来开始设计。面向协议的编程有点不同。在这里,我们通过思考协议来开始我们的设计。然而,正如我们在本章开头所陈述的,面向协议的编程不仅仅是协议。

在本节中,我们将通过当前示例简要讨论的面向协议的编程的不同要素。然后,我们将在接下来的几章中深入讨论这些要素,让您更好地理解如何在应用程序中使用面向协议的编程。

在上一节中,当我们将 Swift 视为面向对象的编程语言时,我们使用类层次结构设计了解决方案,如下图所示:

image.png

要使用面向协议的编程方式重新设计此解决方案,我们需要重新考虑此设计的几个方面。首先重新思考 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,类似于从超类中覆盖函数。

现在让我们创建 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.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"
    }
}

正如我们所看到的,JoltCaffeineFreeDietCoke 类型都是结构而不是类。意味着它们都是值类型,而不是引用类型,就像在面向对象设计中一样。这两种类型都实现了在 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 的建议是在适当的情况下优先使用值类型而不是引用类型。因此,当有疑问时,建议我们使用值类型而不是引用类型。

下图显示了新设计的外观:

image.png

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