面向协议编程

协议(Protocol)是 Swift 的基础功能。在 Swift 的标准库中起着主导作用,并且是一种常见的抽象方法。Protocol 提供了与其他语言类似的接口功能。

这篇文章将介绍面向协议编程(Protocol Oriented Programming,简称 POP),面向协议编程是 Apple 在 WWDC2015 上提出的一种编程范式,其已成为 Swift 的基础。与传统的面向对象编程(Object Oriented Programming,简称 OOP)相比,POP 更为灵活。如果你正在学习 Swift,应掌握面向协议编程。

本文将涉及以下几个方面:

  • 面向对象编程与面向协议编程的区别。
  • 协议的默认实现。
  • 扩展 Swift 标准库。
  • 协议支持范型。

1. 介绍

假设你在开发一款赛车游戏,希望玩家能够驾驶汽车、摩托车和飞机,甚至可以骑不同的鸟飞行。这里的关键是可以操作不同的设备。

一种常见的方案是使用面向对象编程,将所有逻辑封装到基类,其他类继承自基类。因此,基类需要有驾驶、飞行等各种逻辑。

开发过程中为每个设备创建一个类。编程过程中,你会发现CarMotorcycle有一些共用功能,你可能需要创建一个共同的父类MotorVehicle实现共用功能。此外,还会创建一个Aircraft基类实现飞行相关功能,Plane继承自Aircraft

随着需求的迭代,后续可能需要增加会飞的汽车。Swift 不支持多重继承,应如何同时继承自MotorVehicleAircraft?是否需要创建另一个基类,实现MotorVehicleAircraft的功能?当然,也可以通过 Runtime 的消息转发实现多重继承,但其不利于维护,也不优雅。

面向协议编程可以很好解决这一问题。

2. 面向协议编程

协议(protocol)允许将相似的方法、函数、属性放到一组。Swift 中的classenumstruct都可以遵守协议,但只有class支持继承。

与继承相比,协议的优势在于对象可以遵守多个协议。

使用面向协议编程,代码可以更具模块化。可以将协议视为功能块,当通过遵守新的协议添加新功能时,无需创建全新的对象。创建全新的对象太耗费时间。相反,只需增加不同的功能块。

将基类模式转变面向协议编程模式,可以很好解决前面遇到的问题。使用协议时,可以创建一个FlyingCar类,同时遵守MotorVehicleAircraft协议。

3. 创建协议

创建一个名称为ProtocolOrientedProgramming的playground,并添加以下代码:

protocol Bird {
    var name: String { get }
    var canFly: Bool { get }
}

protocol Flyable {
    var airspeedVelocity: Double { get }
}

Bird协议有两个只读的属性。Flyable协议有一个只读的属性。

在没有使用面向协议编程时,开发者一般创建一个Flyable的基类,继承后实现子类。使用面向协议编程后,所有的都以 protocol 开始,将所有功能封装到 protocol,无需使用继承。这样在定义类型时可以更为灵活。

4. 遵守协议

添加以下struct

struct FlappyBird: Bird, Flyable {
    var name: String
    let flappyAmplitude: Double
    let flappyFrequency: Double
    let canFly = true
    
    var airspeedVelocity: Double {
        3 * flappyFrequency * flappyAmplitude
    }
}

FlappyBird结构体遵守了BirdFlyable协议。airspeedVelocity是一个计算属性,FlappyBird是一种会飞的鸟,canFly返回true

继续添加以下结构体:

struct Penguin: Bird {
    let name: String
    let canFly = false
}

struct SwiftBird: Bird, Flyable {
    var name: String { "Swift \(version)"}
    let canFly = true
    let version: Double
    private var speedFactor = 1000.0
    
    init(version: Double) {
        self.version = version
    }
    
    var airspeedVelocity: Double {
        version * speedFactor
    }
}

Penguin是一种不会飞的鸟。如果使用了继承模式,则会让所有鸟会飞。使用协议可以定义一组功能类似的组件,任何相关的对象都可以遵守该协议。

SwiftBird结构体有不同版本,版本越高airspeedVelocity越快。

每个遵守Bird协议的structclass都需要实现canFly,即使已经存在了Flyable协议。如果能为 protocol 提供默认实现,重复代码将变少,这也就是 protocol extension 的用途。

5. Protocol Extension

Protocol extension 提供了协议的默认实现。以下代码为BirdcanFly提供了默认实现:

extension Bird {
    // Flyable birds can fly.
    var canFly: Bool { self is Flyable }
}

遵守Flyable协议的类型canFly返回true,即遵守Bird协议的类型无需重复实现canFly属性。现在可以删除FlappyBirdPenguinSwiftBird中的canFly属性。

6. enum 也可以遵守协议

Swift 中的enum比 C、C++ 中的更为强大,它支持了以往只能够用在类、结构体上的功能。例如,enum可以遵守协议。

添加以下enum

// enum也可以遵守协议
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遵守了BirdFlyable协议,canFly使用了 protocol extension 的默认实现。

7. 重写 protocol extension 的默认实现

UnladenSwallow类型自动使用了Bird协议canFly属性的默认实现,使用以下代码可以重写默认实现:

extension UnladenSwallow {
    var canFly: Bool {
        self != .unknown
    }
}

只有在.african.europeancanFly返回true。使用以下代码进行验证:

UnladenSwallow.unknown.canFly   // false
UnladenSwallow.african.canFly   // true
Penguin(name: "King Penguin").canFly    // false

使用上述方法,可以像面向对象编程一样重写属性、方法。

8. 扩展协议

还可以让自己创建的协议遵守 Swift 标准库中协议,同时定义其默认实现。更新Bird协议如下:

// Bird协议遵守CustomStringConvertible协议。
protocol Bird: CustomStringConvertible {
    var name: String { get }
    var canFly: Bool { get }
}

extension CustomStringConvertible where Self: Bird {
    var description: String {
        canFly ? "I can fly" : "Guess I'll just sit here"
    }
}

Bird协议遵守了CustomStringConvertible协议,CustomStringConvertible协议只有一个实例属性description,实现后可以提供自定义输出。CustomStringConvertible只为Bird类型提供了 protocol extension。

添加以下代码:

UnladenSwallow.african

使用Shift + Command + Enter快捷键运行 playground,可以看到 assistant editor 区域输出的I can fly

9. 使用 protocol extension 扩展 Swift 标准库

Protocol extension 提供了一种扩展命名类的功能,Swift 团队也使用 protocol 改进 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)

上述代码中的sliceArraySlice<Int>类型,而非Array<Int>类型。该包装类型提供了一种快速、高效的方式操作数组的一部分。reversedSliceReversedCollection<ArraySlice<Int>>类型,也是对数组的一种包装。

map函数是在Sequence协议extension中实现的,所有Collection类型都遵守了Sequence协议。因此,可以在ArrayReversedCollection中使用map函数,且使用过程中没有区别。

10. 查找最高分

目前,已经有多种类型遵守Bird协议。下面添加以下代码到 playground:

class Motorcycle {
    init(name: String) {
        self.name = name
        speed = 200.0
    }
    
    var name: String
    var speed: Double
}

Motorcycle类与BirdFlying协议无关,其也可以与其他类型竞赛。

为了统一不同类型,需要一个单独竞赛 protocol,如下所示:

// 声明Racer协议,指定竞赛的指标。
protocol Racer {
    var speed: Double { get }
}

// 下面类型均遵守了Racer协议,即均可以进行比赛。
extension FlappyBird: Racer {
    var speed: Double {
        airspeedVelocity
    }
}

extension SwiftBird: Racer {
    var speed: Double {
        airspeedVelocity
    }
}

extension Penguin: Racer {
    var speed: Double {
        42
    }
}

extension UnladenSwallow: Racer {
    var speed: Double {
        canFly ? airspeedVelocity : 0.0
    }
}

extension Motorcycle: Racer { }

// 数组中实例均遵守了Racer协议
let racers: [Racer] = [
    UnladenSwallow.african,
    UnladenSwallow.european,
    UnladenSwallow.unknown,
    Penguin(name: "King Penguin"),
    SwiftBird(version: 5.1),
    FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
    Motorcycle(name: "Giacomo")
]

10.1 单独方法查找

使用以下函数查找速度最快的竞赛者:

/// 查找速度最快的选手
func topSpeed(of racers: [Racer]) -> Double {
    racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

topSpeed(of: racers)

topSpeed(of:)函数返回最快选手的速度。如果传入数组为空,则返回0.0。执行后其速度是5100。

10.2 范型查找

假设Racers数量众多,目前只需查找部分参与者的最快速度。那么应修改topSpeed(of:)函数参数为Sequence类型,而非数组。如下所示:

// RacersType是范型,需遵守Sequence协议。
// where语句指定Sequence的元素必须遵守Racer协议。
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double where RacersType.Iterator.Element == Racer {
    racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

使用以下代码查看指定范围数组元素速度:

topSpeed(of: racers[1...3])

运行后输出42。该函数目前支持所有Sequence类型,包括ArraySlice

10.3 为 Sequence 增加 extension

还可以进一步优化查找topSpeed选手的方法,优化后如下:

// 当Sequence的元素为Racer类型时,为其添加topSpeed方法。
extension Sequence where Iterator.Element == Racer {
    func topSpeed() -> Double {
        self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
    }
}

racers.topSpeed()
racers[1...3].topSpeed()

参照 Swift 标准库的实现,扩展了Sequence协议,增加了topSpeed()方法,且只有在Sequence元素是Racer类型时可用。

11. 使用协议比较大小

Swift 协议还可以用来比较大小。例如,比较对象是否相等==、大于>和小于<。

添加以下代码:

protocol Score {
    var value: Int { get }
}

struct RacingScore: Score {
    let value: Int
}

有了Score协议,后续所有处理都可以根据Score来进行,无需关注具体类型。

让score可比较就可以很方便的查找到最高分数,更新ScoreRacingScore如下:

protocol Score: Comparable {
    var value: Int { get }
}

struct RacingScore: Score {
    let value: Int
    
    static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
        lhs.value < rhs.value
    }
}

Comparable协议需要提供小于操作的实现。Swift标准库会根据提供的小于操作,自动实现其他类型的比较操作。

RacingScore(value: 150) >= RacingScore(value: 130)  // true

运行后,上述代码打印true

12. mutating

截至目前,所有演示都是在增加功能。如何使用 protocol 改变对象的属性呢?可以使用mutating方法实现,如下所示:

protocol Cheat {
    mutating func boost(_ power: Double)
}

Cheat协议内函数可以修改对象内属性。让SwiftBird遵守Cheat协议,如下所示:

extension SwiftBird: Cheat {
    // 修改speedFactor,让其增加power。
    mutating func boost(_ power: Double) {
        speedFactor += power
    }
}

修改struct结构体内元素时,函数需使用mutating标记。

使用以下代码查看boost(_:)如何工作:

// 创建可变对象
var swiftBird = SwiftBird(version: 5.0)
// 速度增加3
swiftBird.boost(3.0)
swiftBird.airspeedVelocity  // 5015
// 速度再次增加3
swiftBird.boost(3.0)
swiftBird.airspeedVelocity  // 5030

运行后,可以看到SwiftBirdairspeedVelocity速度增加了。

总结

现在已经介绍了面向协议编程的优势。通过默认实现,可以为已经存在的协议提供基础功能。这一点类似于继承中的基类,但可用于structenum

Demo名称:ProtocolOrientedProgramming
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/ProtocolOrientedProgramming

参考资料:

  1. 面向协议编程与 Cocoa 的邂逅 (上)
  2. Protocol-Oriented Programming Tutorial in Swift 5.1: Getting Started
  3. Protocol-Oriented Programming in Swift WWDC2015
  4. Protocol Oriented Programming is Not a Silver Bullet

欢迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/面向协议编程.md

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容