協(xié)議(Protocol)是 Swift 的基礎(chǔ)功能。在 Swift 的標(biāo)準(zhǔn)庫中起著主導(dǎo)作用,并且是一種常見的抽象方法。Protocol 提供了與其他語言類似的接口功能。
這篇文章將介紹面向協(xié)議編程(Protocol Oriented Programming,簡稱 POP),面向協(xié)議編程是 Apple 在 WWDC2015 上提出的一種編程范式,其已成為 Swift 的基礎(chǔ)。與傳統(tǒng)的面向?qū)ο缶幊蹋∣bject Oriented Programming,簡稱 OOP)相比,POP 更為靈活。如果你正在學(xué)習(xí) Swift,應(yīng)掌握面向協(xié)議編程。
本文將涉及以下幾個(gè)方面:
- 面向?qū)ο缶幊膛c面向協(xié)議編程的區(qū)別。
- 協(xié)議的默認(rèn)實(shí)現(xiàn)。
- 擴(kuò)展 Swift 標(biāo)準(zhǔn)庫。
- 協(xié)議支持范型。
1. 介紹
假設(shè)你在開發(fā)一款賽車游戲,希望玩家能夠駕駛汽車、摩托車和飛機(jī),甚至可以騎不同的鳥飛行。這里的關(guān)鍵是可以操作不同的設(shè)備。
一種常見的方案是使用面向?qū)ο缶幊?,將所有邏輯封裝到基類,其他類繼承自基類。因此,基類需要有駕駛、飛行等各種邏輯。
開發(fā)過程中為每個(gè)設(shè)備創(chuàng)建一個(gè)類。編程過程中,你會發(fā)現(xiàn)Car、Motorcycle有一些共用功能,你可能需要?jiǎng)?chuàng)建一個(gè)共同的父類MotorVehicle實(shí)現(xiàn)共用功能。此外,還會創(chuàng)建一個(gè)Aircraft基類實(shí)現(xiàn)飛行相關(guān)功能,Plane繼承自Aircraft。
隨著需求的迭代,后續(xù)可能需要增加會飛的汽車。Swift 不支持多重繼承,應(yīng)如何同時(shí)繼承自MotorVehicle和Aircraft?是否需要?jiǎng)?chuàng)建另一個(gè)基類,實(shí)現(xiàn)MotorVehicle、Aircraft的功能?當(dāng)然,也可以通過 Runtime 的消息轉(zhuǎn)發(fā)實(shí)現(xiàn)多重繼承,但其不利于維護(hù),也不優(yōu)雅。
面向協(xié)議編程可以很好解決這一問題。
2. 面向協(xié)議編程
協(xié)議(protocol)允許將相似的方法、函數(shù)、屬性放到一組。Swift 中的class、enum和struct都可以遵守協(xié)議,但只有class支持繼承。
與繼承相比,協(xié)議的優(yōu)勢在于對象可以遵守多個(gè)協(xié)議。
使用面向協(xié)議編程,代碼可以更具模塊化??梢詫f(xié)議視為功能塊,當(dāng)通過遵守新的協(xié)議添加新功能時(shí),無需創(chuàng)建全新的對象。創(chuàng)建全新的對象太耗費(fèi)時(shí)間。相反,只需增加不同的功能塊。
將基類模式轉(zhuǎn)變面向協(xié)議編程模式,可以很好解決前面遇到的問題。使用協(xié)議時(shí),可以創(chuàng)建一個(gè)FlyingCar類,同時(shí)遵守MotorVehicle和Aircraft協(xié)議。
3. 創(chuàng)建協(xié)議
創(chuàng)建一個(gè)名稱為ProtocolOrientedProgramming的playground,并添加以下代碼:
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
Bird協(xié)議有兩個(gè)只讀的屬性。Flyable協(xié)議有一個(gè)只讀的屬性。
在沒有使用面向協(xié)議編程時(shí),開發(fā)者一般創(chuàng)建一個(gè)Flyable的基類,繼承后實(shí)現(xiàn)子類。使用面向協(xié)議編程后,所有的都以 protocol 開始,將所有功能封裝到 protocol,無需使用繼承。這樣在定義類型時(shí)可以更為靈活。
4. 遵守協(xié)議
添加以下struct:
struct FlappyBird: Bird, Flyable {
var name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
3 * flappyFrequency * flappyAmplitude
}
}
FlappyBird結(jié)構(gòu)體遵守了Bird、Flyable協(xié)議。airspeedVelocity是一個(gè)計(jì)算屬性,FlappyBird是一種會飛的鳥,canFly返回true。
繼續(xù)添加以下結(jié)構(gòu)體:
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是一種不會飛的鳥。如果使用了繼承模式,則會讓所有鳥會飛。使用協(xié)議可以定義一組功能類似的組件,任何相關(guān)的對象都可以遵守該協(xié)議。
SwiftBird結(jié)構(gòu)體有不同版本,版本越高airspeedVelocity越快。
每個(gè)遵守Bird協(xié)議的struct、class都需要實(shí)現(xiàn)canFly,即使已經(jīng)存在了Flyable協(xié)議。如果能為 protocol 提供默認(rèn)實(shí)現(xiàn),重復(fù)代碼將變少,這也就是 protocol extension 的用途。
5. Protocol Extension
Protocol extension 提供了協(xié)議的默認(rèn)實(shí)現(xiàn)。以下代碼為Bird的canFly提供了默認(rèn)實(shí)現(xiàn):
extension Bird {
// Flyable birds can fly.
var canFly: Bool { self is Flyable }
}
遵守Flyable協(xié)議的類型canFly返回true,即遵守Bird協(xié)議的類型無需重復(fù)實(shí)現(xiàn)canFly屬性?,F(xiàn)在可以刪除FlappyBird、Penguin和SwiftBird中的canFly屬性。
6. enum 也可以遵守協(xié)議
Swift 中的enum比 C、C++ 中的更為強(qiáng)大,它支持了以往只能夠用在類、結(jié)構(gòu)體上的功能。例如,enum可以遵守協(xié)議。
添加以下enum:
// enum也可以遵守協(xié)議
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協(xié)議,canFly使用了 protocol extension 的默認(rèn)實(shí)現(xiàn)。
7. 重寫 protocol extension 的默認(rèn)實(shí)現(xiàn)
UnladenSwallow類型自動(dòng)使用了Bird協(xié)議canFly屬性的默認(rèn)實(shí)現(xiàn),使用以下代碼可以重寫默認(rèn)實(shí)現(xiàn):
extension UnladenSwallow {
var canFly: Bool {
self != .unknown
}
}
只有在.african和.european時(shí)canFly返回true。使用以下代碼進(jìn)行驗(yàn)證:
UnladenSwallow.unknown.canFly // false
UnladenSwallow.african.canFly // true
Penguin(name: "King Penguin").canFly // false
使用上述方法,可以像面向?qū)ο缶幊桃粯又貙憣傩浴⒎椒ā?/p>
8. 擴(kuò)展協(xié)議
還可以讓自己創(chuàng)建的協(xié)議遵守 Swift 標(biāo)準(zhǔn)庫中協(xié)議,同時(shí)定義其默認(rèn)實(shí)現(xiàn)。更新Bird協(xié)議如下:
// Bird協(xié)議遵守CustomStringConvertible協(xié)議。
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協(xié)議遵守了CustomStringConvertible協(xié)議,CustomStringConvertible協(xié)議只有一個(gè)實(shí)例屬性description,實(shí)現(xiàn)后可以提供自定義輸出。CustomStringConvertible只為Bird類型提供了 protocol extension。
添加以下代碼:
UnladenSwallow.african
使用Shift + Command + Enter快捷鍵運(yùn)行 playground,可以看到 assistant editor 區(qū)域輸出的I can fly。
9. 使用 protocol extension 擴(kuò)展 Swift 標(biāo)準(zhǔn)庫
Protocol extension 提供了一種擴(kuò)展命名類的功能,Swift 團(tuán)隊(duì)也使用 protocol 改進(jìn) Swift 標(biāo)準(zhǔn)庫。
添加以下代碼:
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是ArraySlice<Int>類型,而非Array<Int>類型。該包裝類型提供了一種快速、高效的方式操作數(shù)組的一部分。reversedSlice是ReversedCollection<ArraySlice<Int>>類型,也是對數(shù)組的一種包裝。
map函數(shù)是在Sequence協(xié)議extension中實(shí)現(xiàn)的,所有Collection類型都遵守了Sequence協(xié)議。因此,可以在Array、ReversedCollection中使用map函數(shù),且使用過程中沒有區(qū)別。
10. 查找最高分
目前,已經(jīng)有多種類型遵守Bird協(xié)議。下面添加以下代碼到 playground:
class Motorcycle {
init(name: String) {
self.name = name
speed = 200.0
}
var name: String
var speed: Double
}
Motorcycle類與Bird、Flying協(xié)議無關(guān),其也可以與其他類型競賽。
為了統(tǒng)一不同類型,需要一個(gè)單獨(dú)競賽 protocol,如下所示:
// 聲明Racer協(xié)議,指定競賽的指標(biāo)。
protocol Racer {
var speed: Double { get }
}
// 下面類型均遵守了Racer協(xié)議,即均可以進(jìn)行比賽。
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 { }
// 數(shù)組中實(shí)例均遵守了Racer協(xié)議
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 單獨(dú)方法查找
使用以下函數(shù)查找速度最快的競賽者:
/// 查找速度最快的選手
func topSpeed(of racers: [Racer]) -> Double {
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
topSpeed(of: racers)
topSpeed(of:)函數(shù)返回最快選手的速度。如果傳入數(shù)組為空,則返回0.0。執(zhí)行后其速度是5100。
10.2 范型查找
假設(shè)Racers數(shù)量眾多,目前只需查找部分參與者的最快速度。那么應(yīng)修改topSpeed(of:)函數(shù)參數(shù)為Sequence類型,而非數(shù)組。如下所示:
// RacersType是范型,需遵守Sequence協(xié)議。
// where語句指定Sequence的元素必須遵守Racer協(xié)議。
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double where RacersType.Iterator.Element == Racer {
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
使用以下代碼查看指定范圍數(shù)組元素速度:
topSpeed(of: racers[1...3])
運(yùn)行后輸出42。該函數(shù)目前支持所有Sequence類型,包括ArraySlice。
10.3 為 Sequence 增加 extension
還可以進(jìn)一步優(yōu)化查找topSpeed選手的方法,優(yōu)化后如下:
// 當(dāng)Sequence的元素為Racer類型時(shí),為其添加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 標(biāo)準(zhǔn)庫的實(shí)現(xiàn),擴(kuò)展了Sequence協(xié)議,增加了topSpeed()方法,且只有在Sequence元素是Racer類型時(shí)可用。
11. 使用協(xié)議比較大小
Swift 協(xié)議還可以用來比較大小。例如,比較對象是否相等==、大于>和小于<。
添加以下代碼:
protocol Score {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
}
有了Score協(xié)議,后續(xù)所有處理都可以根據(jù)Score來進(jìn)行,無需關(guān)注具體類型。
讓score可比較就可以很方便的查找到最高分?jǐn)?shù),更新Score和RacingScore如下:
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協(xié)議需要提供小于操作的實(shí)現(xiàn)。Swift標(biāo)準(zhǔn)庫會根據(jù)提供的小于操作,自動(dòng)實(shí)現(xiàn)其他類型的比較操作。
RacingScore(value: 150) >= RacingScore(value: 130) // true
運(yùn)行后,上述代碼打印true。
12. mutating
截至目前,所有演示都是在增加功能。如何使用 protocol 改變對象的屬性呢?可以使用mutating方法實(shí)現(xiàn),如下所示:
protocol Cheat {
mutating func boost(_ power: Double)
}
Cheat協(xié)議內(nèi)函數(shù)可以修改對象內(nèi)屬性。讓SwiftBird遵守Cheat協(xié)議,如下所示:
extension SwiftBird: Cheat {
// 修改speedFactor,讓其增加power。
mutating func boost(_ power: Double) {
speedFactor += power
}
}
修改struct結(jié)構(gòu)體內(nèi)元素時(shí),函數(shù)需使用mutating標(biāo)記。
使用以下代碼查看boost(_:)如何工作:
// 創(chuàng)建可變對象
var swiftBird = SwiftBird(version: 5.0)
// 速度增加3
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
// 速度再次增加3
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030
運(yùn)行后,可以看到SwiftBird的airspeedVelocity速度增加了。
總結(jié)
現(xiàn)在已經(jīng)介紹了面向協(xié)議編程的優(yōu)勢。通過默認(rèn)實(shí)現(xiàn),可以為已經(jīng)存在的協(xié)議提供基礎(chǔ)功能。這一點(diǎn)類似于繼承中的基類,但可用于struct、enum。
Demo名稱:ProtocolOrientedProgramming
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/ProtocolOrientedProgramming
參考資料:
- 面向協(xié)議編程與 Cocoa 的邂逅 (上)
- Protocol-Oriented Programming Tutorial in Swift 5.1: Getting Started
- Protocol-Oriented Programming in Swift WWDC2015
- Protocol Oriented Programming is Not a Silver Bullet
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/面向協(xié)議編程.md