前言
本文翻译自Introducing Protocol-Oriented Programming in Swift 3
翻译的不对的地方还请多多包涵指正,谢谢~
介绍Swift3中的面向协议编程
设想下你正在开发一个赛跑的游戏。你可以开车,骑摩托或者开飞机。创建这类类型的应用的通用方式是使用面向对象设计,将所有的逻辑封装在对象内,这些对象继承于拥有所有共性的对象。
这种设计模式可行,但会伴随一些缺陷。例如,如你添加创建需要汽油的机器,在背景中飞翔的鸟儿,或任何你希望共享游戏逻辑的事物的能力,没有一种好的方式将车辆的功能性组件分离成一些可复用的事物。
这个场景就是协议大放异彩的时机。
Swift总是让你使用协议来在已有的类,结构体和枚举类型上来指定接口。这样可以让你与它们进行通用的交互。Swift2引入了一个方法来扩展协议并提供默认的实现。最终,Swift3提高了操作一致性且为在标准库中的数字协议使用这些改良。
协议非常强大,能改变你的编码方式。在教程中,你将探索创建&使用协议的方式,且使用面向协议编程使你的代码更加可扩展。
你也可以看到Swift开发组们是怎样使用协议来提升Swift标准库,也可以看到协议是怎样影响你写的代码的。
开始吧
先床架你一个新工作区。在Xcode中,选择File/New/Playground
...并且将工作区命名为SwiftProtocols
。你可以选择任何平台,因为本片教程的所有代码都是平台无关的。点击下一步选择你要保存的地方,最后点击创建按钮。
一旦工作区创建完,添加以下代码:
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
这里定义了一个简单的协议Bird
拥有name, canFly
两个属性,Flyable
协议定义了airspeedVelocity
属性。
在面向协议前的时段,你可能会把Flyable
作为基类,依赖继承定义Bird
子类来表示其他能飞的事物,例如飞机。这里注意,任何事物都是以协议开始的!这样可以让你以一种不需要基类的方式封装功能概念。
之后你开始定义实际类型时,你可以看到协议是如何让整个系统更加灵活的。
定义遵循协议的类型
添加以下结构体定义到工作区底部:
struct FlappyBird: Bird, Flyable {
let name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
return 3 * flappyFrequency * flappyAmplitude
}
}
这里定义了一个新的FlappyBird
类型,并遵循了Bird Flyable
两个协议。它的airspeedVelocity
属性是使用flappyFrequency flappyAmplitude
函数的计算属性。作为可以飞扬的鸟,肯定 canFly
返回 true啦~
下面,在工作区底部添加两个结构体的定义:
struct Penguin: Bird {
let name: String
let canFly = false
}
struct SwiftBird: Bird, Flyable {
var name: String { return "Swift \(version)" }
let version: Double
let canFly = true
// Swift is FASTER every version!
var airspeedVelocity: Double { return version * 1000.0 }
}
企鹅是鸟类,但不能飞。啊哈,没有采取继承的方式使所有鸟儿都能飞真是太好了。使用协议允许你定义功能性组件并让任何相关的对象来遵循它们。
你是不是觉得有些冗余呢。即使已经有一个Flyable
的协议来说明,每个Bird
类型不得不声明它是否canFly
。
使用默认实现来扩展协议
使用协议扩展,你可以为协议定义一个默认的行为。添加如下对Bird
的扩展:
extension Bird {
// Flyable birds can fly!
var canFly: Bool { return self is Flyable }
}
上述对Bird
做了扩展:给canFly
属性设置默认的行为,当Bird
遵循了Flyable
协议属性就是true
。也就是说,任何遵循了Flyable
的Bird
都不需要显示的说明它可以飞了~~
在FlappyBird, SwiftBird, Penguin
结构体内删除 let canFly = ...
的代码,你会发现因为扩展协议默认实现了canFly
代码可以成功的跑起来。
为何不用基类
协议扩展及默认实现看起来和普通基类或者其他语言中的抽象类非常相似,但在Swift中他们有几个关键的优点:
- 因为类型可以遵循多个协议,它们可以使用多个协议的默认实现进行装饰。不像某些语言支持的类的多继承,协议扩展不会引入额外的状态。
- 协议可以被类,结构体,枚举遵循,但是基类只能严格地被类继承。
也就是说,协议扩展可提供为值类型而不仅仅是类提供定义默认行为的能力。
你已经看到在结构体上的处理。下一步,在工作区底部添加枚举的定义:
enum UnladenSwallow: Bird, Flyable {
case african
case european
case unknown
var name: String {
switch self {
case .african:
return "African"
case .european:
return "European"
case .unknown:
return "What do you mean? African or European?"
}
}
var airspeedVelocity: Double {
switch self {
case .african:
return 10.0
case .european:
return 9.9
case .unknown:
fatalError("You are thrown from the bridge of death!")
}
}
}
与任何其他值类型一样,你需要做的就是定义正确的属性,以便UnladenSwallow
符合两个协议。因为它遵循了Bird, Flyable
两种协议,它就获得默认的canFly
且返回true的实现。
你真的认为这篇涉及airspeedVelocity
的教程不会包含Monty Python参考?
覆盖默认的实现
遵循Bird
协议后,UnladenSwallow
类型自动获取到canFly
的实现。但你希望为UnladenSwallow.unknown
类型的canFly
返回false
。有可能覆盖默认的实现吗?是的,当然可以。在工作区添加如下代码:
extension UnladenSwallow {
var canFly: Bool {
return self != .unknown
}
}
现在只有african european
的canFly
才返回false
。将如下代码加到工作区测试下吧~
UnladenSwallow.unknown.canFly // false
UnladenSwallow.african.canFly // true
Penguin(name: "King Penguin").canFly // false
通过这种方式,像面向对象编程中的虚函数一样,也可以通过协议覆盖实现属性和方法。
扩展协议
你可以使用标准库中的协议,并定义默认行为。
修改Bird
协议的定义让它遵循CustomStringConvertible
协议:
protocol Bird: CustomStringConvertible {
遵循CustomStringConvertible
协议意味着你的类型需要有一个string类型的description
属性。那么是不是说每个当前或者之后Bird
类型的所有类型都不得不添加这个属性呢?
当然,通过协议的扩展可以有更简单的方式。在Bird
定义的下方添加如下代码:
extension CustomStringConvertible where Self: Bird {
var description: String {
return canFly ? "I can fly" : "Guess I’ll just sit here :["
}
}
这个扩展使用canFly
属性来生成每个Bird
类型的description
值。
为测试,添加如下代码至工作区底部:
UnladenSwallow.african
可以在辅助编辑器内出现了I can fly!
。但更重要的是,你仅仅扩展了自己的协议~
对Swift标准库的影响
你已经发现协议扩展是一个非常好的用于自定义或扩展能力的方式。你可能会好奇Swift开发团队是怎么利用协议来提升Swift标准库的。
添加如下代码至工作区底部:
let numbers = [10,20,30,40,50,60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()
let answer = reversedSlice.map { $0 * 10 }
print(answer)
以上代码非常简明,你可能已经才想出最后的打印结果。感到奇怪的可能是涉及到的类型。例如,slice
不是integer
的数组(Array)而是ArraySlice
。这种特殊包裹类型扮演着原始数组查看器角色,而且避免了昂贵的能快速添加的内存分配。相对应的,reversedSlice
实际上是ReversedRandomAccessCollection
类型,它也是扮演着原始数组查看器角色。
幸运地是,开发标准库的天才们定义了map
方法作为对Sequence
协议的扩展,且所有结合的包裹类(有很多)都遵循这个协议。这样使得在Array上调用map
协议就像在ReversedRandomAccessCollection
调用一样成为可能,而且看起来没有任何区别。很快你就会借用这个设计模式啦~
去比赛~
目前为止已经定义了多个遵循Bird
协议的类型了。现在在工作区底部添加一些完全不同的东西吧~
class Motorcycle {
init(name: String) {
self.name = name
speed = 200
}
var name: String
var speed: Double
}
目前这个类跟已定义的鸟或者飞翔的东西没有任何关系。但你想骑摩托跟企鹅赛跑。是时候将所有碎片集合起来了~
集合
是时候使用一个协议将不相干的类型归为一致进行比赛了。甚至不用回去修改任何原始数据类型定义,你就可以实现它。花哨的属于是追溯建模。添加如下代码到你的工作区:
protocol Racer {
var speed: Double { get } // speed is the only thing racers care about
}
extension FlappyBird: Racer {
var speed: Double {
return airspeedVelocity
}
}
extension SwiftBird: Racer {
var speed: Double {
return airspeedVelocity
}
}
extension Penguin: Racer {
var speed: Double {
return 42 // full waddle speed
}
}
extension UnladenSwallow: Racer {
var speed: Double {
return canFly ? airspeedVelocity : 0
}
}
extension Motorcycle: Racer {}
let racers: [Racer] =
[UnladenSwallow.african,
UnladenSwallow.european,
UnladenSwallow.unknown,
Penguin(name: "King Penguin"),
SwiftBird(version: 3.0),
FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
Motorcycle(name: "Giacomo")
]
上述代码中,你第一次定义了Racer
协议,且让所有类型实现了该协议。某些类型,诸如Motorcycle
,只是简单的实现了它。其他的,比如UnladenSwallow
,需要写一点逻辑。最终,你有一大串遵循Racer
类型。
因为所有类型都遵循了这个协议,所以你可以创建一个Racer
的数组。
最快速度
现在可以写一个函数来计算赛跑者中最快速度。添加如下代码:
func topSpeed(of racers: [Racer]) -> Double {
return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0
}
topSpeed(of: racers) // 3000
函数使用标准库中max
函数找到赛跑者中最快的速度并返回。如果赛跑者数组为空就返回0。
让其更通用
还有一个问题。假设想找出赛跑者中某部分人的最高速。添加如下代码到工作区会导致错误:
topSpeed(of: racers[1...3]) // ERROR
Swift抱怨对[Racer]
类型无法通过CountableClosedRange
范围值使用下标获取对应的值。切割后返回的是其中一个包裹类型。
解决方案是使用具体的Array
代替通用的协议。如下更改topSpeed
函数:
func topSpeed(of racers: RacerType) -> Double
where RacerType.Iterator.Element == Racer {
return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0
}
这看起来有点吓人,那么让我们分解下。RacerType
对于该函数来说是通用类型,且它可以是任意遵循Swift标准库中Sequence
协议的类型。Where
术语表示序列的元素类型必须遵循Racer
协议。所有遵循Sequence
协议的类型都有一个称为Iterator
且能循环遍历元素类型的关联类型。实际的方法内容体跟之前是一样的。
这个方法可以为任何元素是Sequence
类型的切片数组工作。
topSpeed(of: racers[1...3]) // 42
让其更加Swfit化
你可以做的更好。借用标准库中的玩法,你可以扩展Sequence
类型,这样topSpeed()
可以很容易被发现。添加如下代码到工作区底部:
extension Sequence where Iterator.Element == Racer {
func topSpeed() -> Double {
return self.max(by: { $0.speed < $1.speed })?.speed ?? 0
}
}
racers.topSpeed() // 3000
racers[1...3].topSpeed() // 42
现在你有一个很容易被发现的方法,但仅仅作用于赛跑者序列。
协议比较器
Swift3的一个提升点是创建操作符的方式。
添加如下代码至工作区底部:
protocol Score {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
}
使用Scrore
协议意味着你可以写出用相同方式对待所有的分数(score)。但是,通过拥有不同的诸如RacingScore
的实体类型,可以肯定你不会把这些分数与风格分数或可数分数搞混淆。感谢编译器~
你非常希望分数是可以比较的,这样你能分辨谁拥有最高的分数。在Swift3前,你需要添加全局操作函数来遵循这些协议。现在你可以定义这些静态的作为模型一部分的方法。通过替换Scrore
及RacingScore
的定义来试试吧~
protocol Score: Equatable, Comparable {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
static func ==(lhs: RacingScore, rhs: RacingScore) -> Bool {
return lhs.value == rhs.value
}
static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
return lhs.value < rhs.value
}
}
你刚才在一块区域封装了所有RacingScore
的逻辑。现在你可以比较分数,而且因为有神奇的扩展协议的默认实现,甚至可以使用用你从来没有显示定义的>=
操作符。
RacingScore(value: 150) >= RacingScore(value: 130) // true
何去何从
你可以在这里下载本篇教程所有的代码。
通过创建简单的协议并使用协议扩展进行能力扩展,你已经见识到面向协议编程的强大。使用默认实现,你可以给已存在的协议一个普通或者自动化的行为,类似于基类一样但比它更好,因为它也可以应用于结构体和枚举类型。
另外,协议扩展不单单用于扩展你自己的协议,而且可以为Swift中的标准库,Cocoa,Cocoa Touch 或 任何第三方的协议进行扩展或提供默认实现。
想要继续学习更多关于协议知识,你可以读读official Apple documentation。
你也可以观看WWDC上苹果开发手册的Protocol Oriented Programming视频,深入地了解背后的理论。
操作一致性的理论依据可以在Swift evolution proposal找到。你可能希望学习更多关于Swift集合协议,可以点击 learn how to build your own
最后,伴随着新的编程范式出来,很容易会变得异常兴奋,然后在所有地方都使用它。这篇非常有意思的博客(中文翻译地址)提醒我们警惕银弹解决方案且合理的使用协议。