学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

【译】Protocol Oriented Programming in Swift

在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)
// 4812B485-3965-443B-A76D-72986B0A4FF4

协议继承

一个协议可以继承其他协议,然后在它继承的需求之上添加进一步的需求。在下面的例子中,协议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) {
// try to load from the filesystem based on id
}
}

而不需要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
// 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")

这种设计不仅比将所有需要的功能压缩到单个(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
}

// 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)
}
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
}

底线是您可以选择在类型中遵循哪种协议。类型可以是引用或值类型。如果使用超类实现,则不存在这种灵活性。

另一个好处是我们可以通过协议扩展提供默认实现。实际上,我们甚至可以添加新的功能——这里是最好的部分:我们甚至不需要原始代码。我们可以扩展任何FoundationUIKit协议,并根据我们的需要装饰它,而无需深入研究类结构或其他细节。

总结

Swift支持多种范式:面向对象编程、面向协议编程和函数式编程。这对我们软件开发人员意味着什么?答案是自由。

选择哪种范式取决于您。如果您愿意,您仍然可以走oop路线。你可以混合搭配。然而,一旦你掌握了面向协议编程,你可能就再也不会回头了。

查看我的Pluralsight Swift课程。

谢谢大家,编码愉快!

参考

Protocol Oriented Programming in Swift