面向協(xié)議編程

協(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í)繼承自MotorVehicleAircraft?是否需要?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 中的classenumstruct都可以遵守協(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í)遵守MotorVehicleAircraft協(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)體遵守了BirdFlyable協(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)。以下代碼為BirdcanFly提供了默認(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、PenguinSwiftBird中的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遵守了BirdFlyable協(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)

上述代碼中的sliceArraySlice<Int>類型,而非Array<Int>類型。該包裝類型提供了一種快速、高效的方式操作數(shù)組的一部分。reversedSliceReversedCollection<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類與BirdFlying協(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ù),更新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協(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)行后,可以看到SwiftBirdairspeedVelocity速度增加了。

總結(jié)

現(xiàn)在已經(jīng)介紹了面向協(xié)議編程的優(yōu)勢。通過默認(rèn)實(shí)現(xiàn),可以為已經(jīng)存在的協(xié)議提供基礎(chǔ)功能。這一點(diǎn)類似于繼承中的基類,但可用于structenum。

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

參考資料:

  1. 面向協(xié)議編程與 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/面向協(xié)議編程.md

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容