Swift Protocol 詳解 - 協議&面向協議程式設計

RickeyBoy發表於2017-10-11

之前一個帖子我總結了自己秋招面試經歷,作為一個Swift開發者,有一個非常高頻的問題就是:你覺得Swift相比於其他語言(或者OC來說)的特點和優勢是什麼?作為一個見識短淺的小白來說,這個問題實在是不知如何下手啊。這篇文章,也只是從一個小的角度切入,談一談Swift中的協議Protocol 和 Protocol Oriented Programming。

面向協議程式設計 (Protocol Oriented Programming) 是 Apple 在 2015 年 WWDC 上提出的 Swift 的一種程式設計正規化。下面將從Protocol的基本用法開始講起,最後再分析Protocol在降低程式碼耦合性方面的優勢

Protocol - 協議基本用法

《 The Swift Programming Language 》

Protocol 基礎語法

  1. 屬性要求 :
    • { get set } :指定讀寫屬性
    • static/class:指定型別屬性
  2. 方法要求:
    • static/class:指定類方法
    • mutating:要求實現可變方法(針對值型別的例項方法,可以在該方法中修改它所屬的例項以及例項的任意屬性的值)
  3. 構造器要求:
    • 在遵循協議的類中,必須使用required關鍵字修飾,保證其子類也必須提供該構造器的實現。(除非有final修飾的類,可以不用required,因為不會再有子類)

Protocol 作為型別

  1. 作為型別:代表遵循了該協議的某個例項(實際上就是某個例項遵循了協議)
  2. 協議型別的集合:let A: [someProtocol],遵守某個協議的例項的集合
  3. Delegate 委託設計模式:定義協議來封裝那些需要被委託的功能

Protocol 間的關係

  1. 協議的繼承:協議可繼承
  2. 協議的合成:使用&關鍵字,同時遵循多個協議
  3. 協議的一致性:使用isas?as!進行一致性檢查
  4. 類專屬協議:協議繼承時使用class關鍵字,限制該協議職能被類繼承

optional & @objc 關鍵字

可選協議:使用optional修飾屬性、函式、協議本身,同時所有option必須被@objc修飾,協議本身也必須使用@objc,只能被Objective-C的類或者@objc的類使用

extension 關鍵字

  • (對例項使用)令已有型別遵循某個協議
  • (對協議使用)可遵循其他協議,增加協議一致性
  • (對協議使用)提供預設實現
  • (搭配where對協議使用)增加限制條件

Classes 類 - 特點和問題

類(Class) 是物件導向程式設計之中的重要元素,它代表的是一個共享相同結構和行為的物件的集合

  • Classes 可以做的事:
    • Encapsulation 封裝:表現為對外提供介面,隱藏具體邏輯,保證類的高內聚
    • Access Control 訪問控制:依賴於類的修飾符(如public、private),保證隔離性
    • Abstraction 抽象:提取具有類似特性的事物,進行建模
    • NameSpace 名稱空間:避免不同作用域中,同名變數、函式發生衝突
    • Expressive Syntax 豐富的語法
    • Extensibility 可擴充性:可繼承、可重寫等等

Classes 的問題:

1. Implicit Sharing 隱式共享:

Swift Protocol 詳解 - 協議&面向協議程式設計

可能會導致大量保護性拷貝(Defensive Copy),導致效率降低;也有可能發生競爭條件(race condition),出現不可預知的錯誤;為了避免race condition,需要使用鎖(Lock),但是這更會導致程式碼效率降低,並且有可能導致死鎖(Dead Lock)

2. Inheritance All 全部繼承:

由於繼承時,子類將繼承父類全部的屬性,所以有可能導致子類過於龐大,邏輯過於複雜。尤其是當父類具有儲存屬性(stored properties)的時候,子類必須全部繼承,並且小心翼翼得初始化,避免損壞父類中的邏輯。如果需要重寫(override)父類的方法,則必須要小心思考如何重寫以及何時重寫。

3. Lost Type Relationships 不能反應型別關係:

Swift Protocol 詳解 - 協議&面向協議程式設計

上圖中,兩個類(Label、Number)擁有相同的父類(Ordered),但是在 Number 中呼叫 Order 類必須要使用強制解析(as!)來判斷 Other 的屬性,這樣做既不優雅,也非常容易出Bug(如果 Other 碰巧為Label類)

Coupling or dependency 耦合性

採用面向協議程式設計的方式,可以在一定程度上降低程式碼的耦合性。

耦合性是一種軟體度量,是指一程式中,模組及模組之間資訊或引數依賴的程度。高耦合性將使得維護成本變高,同時降低程式碼可複用程度。低耦合性是結構良好程式的特性,低耦合性程式的可讀性及可維護性會比較好。

耦合級別

Swift Protocol 詳解 - 協議&面向協議程式設計

圖示是耦合程度由高到低,可粗略分為五個級別:

  • Content coupling 內容耦合:又稱病態耦合,一個模組直接使用另一個模組的內部資料。
  • Common coupling 公共耦合:又稱全域性耦合,指通過一個公共資料環境相互作用的那些模組間的耦合。公共耦合的複雜程式隨耦合模組的個數增加而增加。
  • Control coupling 控制耦合:指一個模組呼叫另一個模組時,傳遞的是控制變數(如開關、標誌等),被調模組通過該控制變數的值有選擇地執行塊內某一功能。
  • Stamp coupling 特徵耦合/標記耦合:又稱資料耦合,幾個模組共享一個複雜的資料結構。
  • Data coupling 資料耦合:是指模組藉由傳入值共享資料,每一個資料都是最基本的資料,而且只分享這些資料(例如傳遞一個整數給計算平方根的函式)

高耦合性帶來的問題

  • 維護代價大:修改一個模組時可能產生漣漪效應,其他模組的內部邏輯也需要修改
  • 結構不清晰:由於模組間依賴性太多,所以在模組的組合時需要消耗更多精力
  • 可複用性低:每一個模組的依賴模組太多,導致可複用的程度降低

解耦 - Dependency Inversion Principle 依賴反轉原則

傳統的依賴關係建立在高層次上,而具體的策略設定則應用在低層次的模組上,採用繼承的方式實現。依賴反轉原則(DIP)是指一種特定的解耦方式,使得高層次的模組不依賴於低層次的模組的實現細節,依賴關係被顛倒(反轉),從而使得低層次模組依賴於高層次模組的需求抽象。

DIP 規定:

  • 高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面。
  • 抽象介面不應該依賴於具體實現。而具體實現則應該依賴於抽象介面。
Swift Protocol 詳解 - 協議&面向協議程式設計 Swift Protocol 詳解 - 協議&面向協議程式設計

舉一個簡單而經典的例子 -- 檯燈和按鈕

第一幅圖為傳統的實現方式,依賴關係被建立直接在高層次物件(Button)上,當你需要改變低層次物件(Lamp)時,你必須要同時更改其父類(Button),如果此時有多個低層次的物件繼承自父類(Button),那麼更改其父類就變得十分困難。而第二幅圖是符合DIP原則的方式,高層物件(Button)把需求抽象為一個抽象介面(ButtonServer),而具體實現(Lamp)依賴於這個抽象介面。同時,當需要實現多個底層物件時,只需要在具體實現時進行不同的實現即可。

解耦 - Protocol Oriented Programming 面向協議程式設計

面向協議程式設計中,Protocol 實際上就是 DIP 中的抽象介面。通過之前的講解,採用面向協議的方式進行程式設計,即是對依賴反轉原則 DIP 的踐行,在一定程度上降低程式碼的耦合性,避免耦合性過高帶來的問題。下面通過一個具體例項簡單講解一下:

首先是高層次結構的實現,建立EmmettBrown的類,然後宣告瞭一個需求(travelInTime方法)。

// 高層次實現 - EmmettBrown
final class EmmettBrown {
	private let timeMachine: TimeTraveling
	init(timeMachine: TimeTraveling) {
		self.timeMachine = timeMachine
	}
	func travelInTime(time: TimeInterval) -> String {
		return timeMachine.travelInTime(time: time)
	}
}
複製程式碼

採用 Protocol 定義抽象介面 travelInTime,低層次的實現將需要依賴這個介面。

// 抽象介面 - 時光旅行
protocol TimeTraveling {
    func travelInTime(time: TimeInterval) -> String
}
複製程式碼

最後是低層次實現,建立DeLorean類,通過遵循TimeTraveling協議,完成TravelInTime抽象介面的具體實現。

// 低層次實現 - DeLorean
final class DeLorean: TimeTraveling {
	func travelInTime(time: TimeInterval) -> String {
		return "Used Flux Capacitor and travelled in time by: \(time)s"
	}
}
複製程式碼

使用的時候只需要建立相關類即可呼叫其方法。

// 使用方式
let timeMachine = DeLorean()
let mastermind = EmmettBrown(timeMachine: timeMachine)
mastermind.travelInTime(time: -3600 * 8760)
複製程式碼

Delegate - 利用 Protocol 解耦

Delegation is a design pattern that enables a class or structure to hand off (or delegate) some of its responsibilities to an instance of another type.

委託(Delegate)是一種設計模式,表示將一個物件的部分功能轉交給另一個物件。委託模式可以用來響應特定的動作,或者接收外部資料來源提供的資料,而無需關心外部資料來源的型別。部分情況下,Delegate 比起自上而下的繼承具有更鬆的耦合程度,有效的減少程式碼的複雜程度。

那麼 Deleagte 和 Protocol 之間是什麼關係呢?在 Swift 中,Delegate 就是基於 Protocol 實現的,定義 Protocol 來封裝那些需要被委託的功能,這樣就能確保遵循協議的型別能提供這些功能。

Protocol 是 Swift 的語言特性之一,而 Delegate 是利用了 Protocol 來達到解耦的目的。

Delegate 使用例項:

//定義一個委託
protocol CustomButtonDelegate: AnyObject{
    func CustomButtonDidClick()
}
 
class ACustomButton: UIView {
    ...
    weak var delegate: ButtonDelegate?
    func didClick() {
        delegate?.CustomButtonDidClick()
    }
}

// 遵循委託的類
class ViewController: UIViewController, CustomButtonDelegate {
    let view = ACustomButton()
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        view.delegate = self
    }
    func CustomButtonDidClick() {
        print("Delegation works!")
    }
}
複製程式碼

程式碼說明

如前所述,Delegate 的原理其實很簡單。ViewController 會將 ACustomButtondelegate 設定為自己,同時自己遵循、實現了 CustomButtonDelegate 協議中的方法。這樣在後者呼叫 didClick 方法的時候會呼叫 CustomButtonDidClick 方法,從而觸發前者中對應的方法,從而列印出 Delegation works!

迴圈引用

我們注意到,在宣告委託時,我們使用了 weak 關鍵字。目的是在於避免迴圈引用。ViewController 擁有 view,而 view.delegate 又強引用了 ViewController,如果不將其中一個強引用設定為弱引用,就會造成迴圈引用的問題。

AnyObject

定義委託時,我們讓 protocol 繼承自 AnyObject。這是由於,在 Swift 中,這表示這一個協議只能被應用於 class(而不是 struct 和 enum)。

實際上,如果讓 protocol 不繼承自任何東西,那也是可以的,這樣定義的 Delegate 就可以被應用於 class 以及 struct、enum。由於 Delegate 代表的是遵循了該協議的例項,所以當 Delegate 被應用於 class 時,它就是 Reference type,需要考慮迴圈引用的問題,因此就必須要用 weak 關鍵字。

但是這樣的問題在於,當 Delegate 被應用於 struct 和 enum 時,它是 Value type,不需要考慮迴圈引用的問題,也不能被使用 weak 關鍵字。所以當 Delegate 未限定只能用於 class,Xcode 就會對 weak 關鍵字報錯:'weak' may only be applied to class and class-bound protocol types

那麼為什麼不使用 class 和 NSObjectProtocol,而要使用 AnyObject 呢?NSObjectProtocol 來自 Objective-C,在 pure Swift 的專案中並不推薦使用。class 和 AnyObject 並沒有什麼區別,在 Xcode 中也能達到相同的功能,但是官方還是推薦使用 AnyObject。

參考資料

相關文章