Swift 中的面向协议编程
介绍
什么是 POP,为什么要使用它,它在现实中如何发挥作用?
Swift - 第一种 POP 语言
在 2015 年 WWDC 上,苹果宣布 Swift 是世界上第一个面向协议编程(POP)语言。
那么 POP 是什么?
面向协议编程是 Swift 2.0 引入的一种新编程范式。在面向协议的方法中,我们通过定义协议来开始设计我们的系统。我们依赖新概念:协议扩展、协议继承和协议组合。该范式还改变了我们看待语义的方式。在 Swift 中,值类型优于类。但是,面向对象的概念不适用于结构和枚举:结构不能从另一个结构继承,枚举也不能从另一个枚举继承。因此,继承 fa(面向对象的基本概念之一)不能应用于值类型。另一方面,值类型可以从协议甚至多个协议继承。因此,使用 POP,值类型已成为 Swift 中的一等公民。
从协议开始
在设计软件系统时,我们会尝试确定满足给定系统要求所需的元素。然后,我们为这些元素之间的关系建模。我们可以从超类开始,并通过继承为其关系建模。或者,我们可以从协议开始,并将关系建模为协议实现。Swift 为这两种解释提供了全面支持。但是,Apple 告诉我们:
“不要从课程开始,而要从协议开始。”
为何?因为协议比类更能起到抽象的作用。
如果使用类来建模抽象,则需要依赖继承。超类定义核心功能并将其公开给子类。子类可以完全覆盖该行为、添加特定行为或让超类完成所有工作。这很好用,直到您意识到需要来自其他类的更多功能。Swift 与许多其他编程语言一样,不支持多重继承。按照类优先的方法,您必须不断向超类添加新功能或以其他方式创建新的中间类,从而使问题复杂化。另一方面,协议充当蓝图而不是父类。协议通过描述实现类型应实现的内容来建模抽象。让我们以以下协议为例:
protocol Entity {
var name: String {get set}
static func uid() -> String
}
它告诉我们,该协议的采用者将能够通过实现类型方法 uid() 来创建一个实体、为其分配一个名称并生成其唯一标识符。
一种类型可以建模多个抽象,因为任何类型(包括值类型)都可以实现多个协议。与类继承相比,这是一个巨大的优势。您可以根据需要创建尽可能多的协议和协议扩展来分离关注点。告别单片超类吧!唯一的警告是协议抽象地定义模板——没有实现。这就是协议扩展可以拯救的地方。
POP 的支柱
协议扩展
协议充当蓝图:它们告诉我们采用者应实现什么,但您无法在协议中提供实现。如果我们需要为符合类型定义默认行为怎么办?我们需要在基类中实现它,对吗?错!必须依赖基类来实现默认实现会削弱协议的优势。此外,这对值类型不起作用。幸运的是,还有另一种方法:协议扩展是可行的方法!在 Swift 中,您可以扩展协议并为方法、计算属性、下标和便利初始化器提供默认实现。在下面的示例中,我为类型方法 uid() 提供了默认实现。
extension Entity {
static func uid() -> String {
return UUID().uuidString
}
}
现在采用该协议的类型不再需要实现 uid() 方法。
struct Order: Entity {
var name: String
let uid: String = Order.uid()
}
let order = Order(name: "My Order")
print(order.uid)
// 4812B485-3965-443B-A76D-72986B0A4FF4
协议继承
协议可以继承自其他协议,然后在继承的要求之上添加进一步的要求。在下面的示例中,Persistable 协议继承自我之前介绍的 Entity 协议。它添加了将实体保存到文件并根据其唯一标识符加载它的要求。
protocol Persistable: Entity {
func write(instance: Entity, to filePath: String)
init?(by uid: String)
}
采用 Persistable 协议的类型必须满足 Entity 和 Persistable 协议中定义的要求。
如果您的类型需要持久性功能,它应该实现 Persistable 协议。
struct PersistableEntity: Persistable {
var name: String
func write(instance: Entity, to filePath: String) { // ...
}
init?(by uid: String) {
// try to load from the filesystem based on id
}
}
而不需要持久化的类型只需实现实体协议:
struct InMemoryEntity: Entity {
var name: String
}
协议继承是一项强大的功能,它允许更细粒度和更灵活的设计。
协议组成
Swift 不允许类多重继承。但是,Swift 类型可以采用多个协议。有时您可能会发现此功能很有用。
这里有一个例子:假设我们需要一个代表实体的类型。
我们还需要比较给定类型的实例。我们还想提供自定义描述。
我们有三个协议来定义所提到的要求:
- 实体
- 可等价
- 自定义字符串可转换
如果这些是基类,我们必须将功能合并到一个超类中;但是,通过 POP 和协议组合,解决方案变成:
struct MyEntity: Entity, Equatable, CustomStringConvertible {
var name: String
// Equatable
public static func ==(lhs: MyEntity, rhs: MyEntity) -> Bool {
return lhs.name == rhs.name
}
// CustomStringConvertible
public var description: String {
return "MyEntity: \(name)"
}
}
let entity1 = MyEntity(name: "42")
print(entity1)
let entity2 = MyEntity(name: "42")
assert(entity1 == entity2, "Entities shall be equal")
这种设计不仅比将所有必需的功能压缩到一个单一的基类中更灵活,而且适用于值类型。
使用 POP 实现更简洁的设计
我将通过一个示例向您展示面向协议编程相对于传统方法的优势。
我们的目标是创建满足以下要求的类型:
- 创建给定名称和图像数据的图像
- 图像应该保存到文件系统并从文件系统加载
- 创建图像的有损压缩版本
- 对图像进行 Base64 编码,以便通过互联网传输
超类方式
让我们从基类方法开始。我将所有功能都压缩到 Image 类中。我们得到的是一个完整的类型:我们可以直接创建 Image 类的实例,并且满足所有要求。
class Image {
fileprivate var imageName: String
fileprivate var imageData: Data
var name: String {
return imageName
}
init(name: String, data: Data) {
imageName = name
imageData = data
}
// persistence
func save(to url: URL) throws {
try self.imageData.write(to: url)
}
convenience init(name: String, contentsOf url: URL) throws {
let data = try Data(contentsOf: url)
self.init(name: name, data: data)
}
// compression
convenience init?(named name: String, data: Data, compressionQuality: Double) {
guard let image = UIImage.init(data: data) else { return nil }
guard let jpegData = UIImageJPEGRepresentation(image, CGFloat(compressionQuality)) else { return nil }
self.init(name: name, data: jpegData)
}
// BASE64 encoding
var base64Encoded: String {
return imageData.base64EncodedString()
}
}
// Test
var image = Image(name: "Pic", data: Data(repeating: 0, count: 100))
print(image.base64Encoded)
do {
// persist image
let documentDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let imageURL = documentDirectory.appendingPathComponent("MyImage")
try image.save(to: imageURL)
print("Image saved successfully to path \(imageURL)")
// load image from persistence
let storedImage = try Image.init(name: "MyRestoredImage", contentsOf: imageURL)
print("Image loaded successfully from path \(imageURL)")
} catch {
print(error)
}
现在,如果我们不需要所有这些功能怎么办?假设我并不总是需要 Base64 编码功能。如果我将 Image 类子类化,我将获得所有功能 - 即使我不需要它们。
如果我们需要创建子类来专门化某些方法,则无法摆脱那些我们不需要的公共方法和属性。我们只能在继承时获得一切。
此外,我们仅限于课程。现在,让我们使用 POP 来改造这个设计。
使用 POP 进行重新设计
我将为每个主要功能创建协议,即持久性、创建压缩、有损版本和 Base64 编码。
protocol NamedImageData {
var name: String { get }
var data: Data { get }
init(name: String, data: Data)
}
protocol ImageDataPersisting: NamedImageData {
init(name: String, contentsOf url: URL) throws
func save(to url: URL) throws
}
extension ImageDataPersisting {
init(name: String, contentsOf url: URL) throws {
let data = try Data(contentsOf: url)
self.init(name: name, data: data)
}
func save(to url: URL) throws {
try self.data.write(to: url)
}
}
protocol ImageDataCompressing: NamedImageData {
func compress(withQuality compressionQuality: Double) -> Self?
}
extension ImageDataCompressing {
func compress(withQuality compressionQuality: Double) -> Self? {
guard let uiImage = UIImage.init(data: self.data) else {
return nil
}
guard let jpegData = UIImageJPEGRepresentation(uiImage, CGFloat(compressionQuality)) else {
return nil
}
return Self(name: self.name, data: jpegData)
}
}
protocol ImageDataEncoding: NamedImageData {
var base64Encoded: String { get }
}
extension ImageDataEncoding {
var base64Encoded: String {
return self.data.base64EncodedString()
}
}
通过这种方法,我们可以创建更细粒度的设计。您可以创建一个采用所有协议的类型:
struct MyImage: ImageDataPersisting, ImageDataCompressing, ImageDataEncoding {
var name: String
var data: Data
}
或者您可能决定跳过对ImageDataPersisting的符合性:
struct InMemoryImage: NamedImageData, ImageDataCompressing, ImageDataEncoding {
var name: String
var data: Data
}
最重要的是,您可以选择在类型中采用哪种协议。并且您的类型可以是引用或值类型。如果使用超类实现,则不存在这种灵活性。
另一个好处是我们可以通过协议扩展提供默认实现。实际上,我们甚至可以添加新功能 - 最好的部分是:我们甚至不需要原始代码。我们可以扩展任何 Foundation 或 UIKit 协议并根据需要对其进行装饰,而无需深入研究类结构或其他细节。
最后的想法
Swift 支持多种范式:面向对象编程、面向协议编程和函数式编程。这对于我们软件开发人员来说意味着什么?答案是自由。
选择哪种范式取决于你。如果你愿意,你仍然可以走 OOP 路线。你可以混合搭配。然而,一旦你理解了面向协议编程,你可能就再也不会回头了。
谢谢!祝您编码愉快!
致谢
我要感谢 Apple 的 Swift 和开发者工具推广者 Marshall Elfstrand 提出的宝贵建议。
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~