面向協(xié)議編程

協(xié)議(Protocol)是 Swift 的基礎(chǔ)功能。在 Swift 的標(biāo)準(zhǔn)庫(kù)中起著主導(dǎo)作用掀泳,并且是一種常見的抽象方法况既。Protocol 提供了與其他語(yǔ)言類似的接口功能。

這篇文章將介紹面向協(xié)議編程(Protocol Oriented Programming属瓣,簡(jiǎn)稱 POP)载迄,面向協(xié)議編程是 Apple 在 WWDC2015 上提出的一種編程范式讯柔,其已成為 Swift 的基礎(chǔ)。與傳統(tǒng)的面向?qū)ο缶幊蹋∣bject Oriented Programming护昧,簡(jiǎn)稱 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)庫(kù)湿酸。
  • 協(xié)議支持范型。

1. 介紹

假設(shè)你在開發(fā)一款賽車游戲灭美,希望玩家能夠駕駛汽車推溃、摩托車和飛機(jī),甚至可以騎不同的鳥飛行届腐。這里的關(guān)鍵是可以操作不同的設(shè)備铁坎。

一種常見的方案是使用面向?qū)ο缶幊蹋瑢⑺羞壿嫹庋b到基類梯捕,其他類繼承自基類厢呵。因此,基類需要有駕駛傀顾、飛行等各種邏輯襟铭。

開發(fā)過程中為每個(gè)設(shè)備創(chuàng)建一個(gè)類。編程過程中短曾,你會(huì)發(fā)現(xiàn)Car寒砖、Motorcycle有一些共用功能,你可能需要?jiǎng)?chuàng)建一個(gè)共同的父類MotorVehicle實(shí)現(xiàn)共用功能嫉拐。此外哩都,還會(huì)創(chuàng)建一個(gè)Aircraft基類實(shí)現(xiàn)飛行相關(guān)功能,Plane繼承自Aircraft婉徘。

隨著需求的迭代漠嵌,后續(xù)可能需要增加會(huì)飛的汽車。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 中的class府寒、enumstruct都可以遵守協(xié)議魁衙,但只有class支持繼承报腔。

與繼承相比株搔,協(xié)議的優(yōu)勢(shì)在于對(duì)象可以遵守多個(gè)協(xié)議。

使用面向協(xié)議編程纯蛾,代碼可以更具模塊化纤房。可以將協(xié)議視為功能塊翻诉,當(dāng)通過遵守新的協(xié)議添加新功能時(shí)炮姨,無(wú)需創(chuàng)建全新的對(duì)象。創(chuàng)建全新的對(duì)象太耗費(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抹镊,無(wú)需使用繼承。這樣在定義類型時(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是一種會(huì)飛的鳥氨菇,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是一種不會(huì)飛的鳥。如果使用了繼承模式查蓉,則會(huì)讓所有鳥會(huì)飛乌询。使用協(xié)議可以定義一組功能類似的組件,任何相關(guān)的對(duì)象都可以遵守該協(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é)議的類型無(wú)需重復(fù)實(shí)現(xiàn)canFly屬性。現(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)庫(kù)中協(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)庫(kù)

Protocol extension 提供了一種擴(kuò)展命名類的功能灶体,Swift 團(tuán)隊(duì)也使用 protocol 改進(jìn) Swift 標(biāo)準(zhǔn)庫(kù)。

添加以下代碼:

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>>類型樟结,也是對(duì)數(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é)議無(wú)關(guān)前鹅,其也可以與其他類型競(jìng)賽。

為了統(tǒng)一不同類型峭梳,需要一個(gè)單獨(dú)競(jìng)賽 protocol舰绘,如下所示:

// 聲明Racer協(xié)議,指定競(jìng)賽的指標(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ù)查找速度最快的競(jìng)賽者:

/// 查找速度最快的選手
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語(yǔ)句指定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)庫(kù)的實(shí)現(xiàn)姿骏,擴(kuò)展了Sequence協(xié)議,增加了topSpeed()方法斤彼,且只有在Sequence元素是Racer類型時(shí)可用分瘦。

11. 使用協(xié)議比較大小

Swift 協(xié)議還可以用來比較大小。例如琉苇,比較對(duì)象是否相等==嘲玫、大于>和小于<。

添加以下代碼:

protocol Score {
    var value: Int { get }
}

struct RacingScore: Score {
    let value: Int
}

有了Score協(xié)議并扇,后續(xù)所有處理都可以根據(jù)Score來進(jìn)行去团,無(wú)需關(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)庫(kù)會(huì)根據(jù)提供的小于操作,自動(dòng)實(shí)現(xiàn)其他類型的比較操作肴熏。

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

運(yùn)行后鬼雀,上述代碼打印true

12. mutating

截至目前蛙吏,所有演示都是在增加功能源哩。如何使用 protocol 改變對(duì)象的屬性呢?可以使用mutating方法實(shí)現(xiàn)鸦做,如下所示:

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

Cheat協(xié)議內(nèi)函數(shù)可以修改對(duì)象內(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)建可變對(duì)象
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)勢(shì)矢炼。通過默認(rèn)實(shí)現(xiàn)系瓢,可以為已經(jīng)存在的協(xié)議提供基礎(chǔ)功能。這一點(diǎn)類似于繼承中的基類句灌,但可用于struct夷陋、enum欠拾。

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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市骗绕,隨后出現(xiàn)的幾起案子藐窄,更是在濱河造成了極大的恐慌,老刑警劉巖酬土,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荆忍,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡撤缴,警方通過查閱死者的電腦和手機(jī)刹枉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屈呕,“玉大人微宝,你說我怎么就攤上這事』⒄#” “怎么了蟋软?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)嗽桩。 經(jīng)常有香客問我岳守,道長(zhǎng),這世上最難降的妖魔是什么涤躲? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任棺耍,我火速辦了婚禮,結(jié)果婚禮上种樱,老公的妹妹穿的比我還像新娘。我一直安慰自己俊卤,他們只是感情好嫩挤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著消恍,像睡著了一般岂昭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狠怨,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天约啊,我揣著相機(jī)與錄音,去河邊找鬼佣赖。 笑死恰矩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的憎蛤。 我是一名探鬼主播外傅,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纪吮,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了萎胰?” 一聲冷哼從身側(cè)響起碾盟,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎技竟,沒想到半個(gè)月后冰肴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡榔组,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年嚼沿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓷患。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡骡尽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出擅编,到底是詐尸還是另有隱情攀细,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布爱态,位于F島的核電站谭贪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏锦担。R本人自食惡果不足惜俭识,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望洞渔。 院中可真熱鬧套媚,春花似錦、人聲如沸磁椒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)浆熔。三九已至本辐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間医增,已是汗流浹背慎皱。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留叶骨,地道東北人茫多。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像邓萨,于是被迫代替她去往敵國(guó)和親地梨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子菊卷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容