在2015年的全球开发者大会上,苹果公司宣布Swift是世界上第一种面向协议的编程(POP)语言
什么是POP? 面向协议编程是Swift 2.0
引入的一种新的编程范式。在面向协议的方法中,我们通过定义协议开始设计我们的系统。我们依赖于新的概念:协议扩展
、协议继承
和协议组合
。范式也改变了我们对语义的看法。在Swift中,值类型优先于类 。然而,面向对象的概念不能很好地与结构体和枚举一起工作:一个结构体不能从另一个结构体继承,一个enum也不能从另一个enum继承。因此继承—一个基本的面向对象的概念—不能应用于值类型。另一方面,值类型可以从协议继承 ,甚至可以从多个协议继承 。因此,有了POP,值类型就成为了Swift的头等公民。
从协议开始 在设计软件系统时,我们试图确定满足给定系统需求所需的元素。然后我们为这些元素之间的关系建模。我们可以从一个超类(superclass)开始,通过继承来建模它的关系。或者我们可以从一个协议开始,将关系建模为一个协议实现。Swift为这两种处理提供了完全支持。然而,苹果告诉我们:
“不要从类开始,要从协议开始。”
为什么?协议是比类更好的抽象。
如果使用类为抽象建模,则需要依赖继承。超类定义核心功能并暴露给子类。子类可以完全覆盖该行为,添加特定的行为,或者由父类完成所有工作。这工作得很好,直到您意识到需要从不同的类中获得更多功能。与许多其他编程语言一样,Swift不支持多重继承 。为了遵循类优先的处理,您必须不断向超类添加新功能,或者创建新的中介类,从而使问题复杂化。另一方面,协议是模版而不是父母(parents)。协议通过描述实现类型应该实现什么来对抽象进行建模。让我们以以下协议为例:
1 2 3 4 protocol Entity { var name: String {get set } static func uid () -> String }
由上可知,该协议的采用者将能够通过实现类型方法uid()
创建一个实体,为它分配一个名称并生成它的唯一标识符。
一种类型可以对多个抽象建模,因为任何类型——包括值类型——都可以实现多个协议。与类继承相比,这是一个巨大的好处。您可以根据需要创建尽可能多的协议和协议扩展来分离这些关注点。跟单一(monolithic)超类说再见!唯一需要注意的是,协议抽象地定义了模板——没有实现。这时协议扩展就派上用场了。
协议编程的支柱 – 协议扩展 协议就像模版:它们告诉我们采用者应该实现什么,但是您不能在协议中提供实现。如果我们需要为符合规范的类型定义默认行为呢?我们需要在基类中实现它,对吧?错了!必须依赖基类进行默认实现将使协议的好处黯然失色。此外,这对值类型不起作用。幸运的是,还有另一种方法:协议扩展是可行的!在Swift中,您可以扩展协议,并为方法、计算属性、下标和便利初始化器提供默认实现。在下面的示例中,我为类型方法uid()
提供了默认实现。
1 2 3 4 5 extension Entity { static func uid () -> String { return UUID ().uuidString } }
现在,采用该协议的类型不再需要实现uid()
方法。
1 2 3 4 5 6 7 struct Order : Entity { var name: String let uid: String = Order .uid() } let order = Order (name: "My Order" )print (order.uid)
协议继承 一个协议可以继承其他协议,然后在它继承的需求之上添加进一步的需求。在下面的例子中,协议persistent
继承自我前面介绍的Entity
协议。它添加了将实体保存到文件并根据其惟一标识符加载它的需求。
1 2 3 4 protocol Persistable : Entity { func write (instance : Entity , to filePath : String ) init? (by uid : String ) }
遵循Persistable
协议的类型必须满足Entity
和Persistable
协议中定义的需求。
如果您的类型需要persistence
功能,那么它应该实现Persistable
协议。
1 2 3 4 5 6 7 8 struct PersistableEntity : Persistable { var name: String func write (instance : Entity , to filePath : String ) { } init? (by uid : String ) { } }
而不需要persisted
的类型只能实现Entity
协议:
1 2 3 struct InMemoryEntity : Entity { var name: String }
协议继承是一个功能强大的特性,因为它支持更细粒度和更灵活的设计。
协议组成 Swift不允许类的多重继承。但Swift类型可以遵循多种协议。有时您可能会发现这个特性很有用。
下面是一个例子:假设我们需要一个表示Entity
的类型。
我们还需要比较给定类型的实例。我们还想提供一个自定义描述。
我们有三个协议来定义上述的需求:
Entity
Equatable
CustomStringConvertible 如果这些是基类,我们必须将功能合并到一个超类中;然而,有了POP和协议组合,解决方案变成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct MyEntity : Entity , Equatable , CustomStringConvertible { var name: String public static func == (lhs : MyEntity , rhs : MyEntity ) -> Bool { return lhs.name == rhs.name } 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" )
这种设计不仅比将所有需要的功能压缩到单个(monolithic)基类中更灵活,而且也适用于值类型。
更干净的POP设计示例 我将通过一个示例向您展示面向协议编程相对于传统方法的好处。
我们的目标是创建满足以下要求的类型:
创建一个给定名称和图像数据的图像
图像应该持久化到文件系统并从文件系统加载
创建图像的有损压缩版本
Base64对图像进行编码,以便在因特网上传输它
使用超类的方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class Image { fileprivate var imageName: String fileprivate var imageData: Data var name: String { return imageName } init (name : String , data : Data ) { imageName = name imageData = data } 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) } 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) } var base64Encoded: String { return imageData.base64EncodedString() } } var image = Image (name: "Pic" , data: Data (repeating: 0 , count: 100 ))print (image.base64Encoded)do { 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) " ) let storedImage = try Image .init (name: "MyRestoredImage" , contentsOf: imageURL) print ("Image loaded successfully from path \(imageURL) " ) } catch { print (error) } swift
现在,如果我们不需要所有这些功能呢?假设我并不总是需要Base64
编码功能。如果我对Image
类进行子类化,我将获得所有的特性—即使我不需要它们。
如果我们需要创建子类来专门化(specialize)某些方法,就没有办法去除那些我们不需要的公共方法和属性。当我们继承的时候,我们得到了一切。
此外,我们受限于classes。现在,让我们使用POP修改这个设计。
用POP重新设计 我将为每个主要特性创建协议,即持久性、创建压缩的有损版本和Base64编码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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() } }
通过这种方法,我们可以创造出更细粒度的设计。你可以创建一个遵循所有协议的类型:
1 2 3 4 struct MyImage : ImageDataPersisting , ImageDataCompressing , ImageDataEncoding { var name: String var data: Data }
或者您可以决定跳过遵从 ImageDataPersisting
:
1 2 3 4 struct InMemoryImage : NamedImageData , ImageDataCompressing , ImageDataEncoding { var name: String var data: Data }
底线是您可以选择在类型中遵循哪种协议。类型可以是引用或值类型。如果使用超类实现,则不存在这种灵活性。
另一个好处是我们可以通过协议扩展提供默认实现。实际上,我们甚至可以添加新的功能——这里是最好的部分:我们甚至不需要原始代码。我们可以扩展任何Foundation
或UIKit
协议,并根据我们的需要装饰它,而无需深入研究类结构或其他细节。
总结 Swift支持多种范式:面向对象编程、面向协议编程和函数式编程。这对我们软件开发人员意味着什么?答案是自由。
选择哪种范式取决于您。如果您愿意,您仍然可以走oop路线。你可以混合搭配。然而,一旦你掌握了面向协议编程,你可能就再也不会回头了。
查看我的Pluralsight Swift课程。
谢谢大家,编码愉快!
参考 Protocol Oriented Programming in Swift