面向協(xié)議編程
你可能聽過類似的概念:面向?qū)ο缶幊?/strong>慈俯、函數(shù)式編程栏尚、泛型編程起愈,再加上蘋果新提出的面向協(xié)議編程,這些統(tǒng)統(tǒng)可以理解為是一種編程范式译仗。所謂編程范式抬虽,是隱藏在編程語言背后的思想,代表著語言的作者想要用怎樣的方式去解決怎樣的問題纵菌。
不同的編程范式反應(yīng)在現(xiàn)實世界中阐污,就是不同的編程語言適用于不同的領(lǐng)域和環(huán)境,比如在面向?qū)ο缶幊趟枷胫性墼玻_發(fā)者用對象來描述萬事萬物并試圖用對象來解決所有可能的問題笛辟。編程范式都有其各自的偏好和使用限制,所以越來越多的現(xiàn)代編程語言開始支持多范式序苏,使語言自身更強壯也更具適用性手幢。
面向協(xié)議編程是在面向?qū)ο缶幊袒A(chǔ)上演變而來,將程序設(shè)計過程中遇到的數(shù)據(jù)類型的抽瘸老辍(抽象)由使用基類進行抽取改為使用協(xié)議進行抽取弯菊。更簡單點舉個例子來說,一個貓類、一個狗類管钳,我們很容易想到抽取一個描述動物的基類钦铁,這就是面向?qū)ο缶幊獭.?dāng)然也會有人想到抽取一個動物通用的協(xié)議才漆,這就是面向協(xié)議編程了牛曹。
而在Swift語言中,協(xié)議被賦予了更多的功能和更廣闊的使用空間醇滥,為協(xié)議增加了擴展功能黎比,使其能夠勝任絕大多數(shù)情況下數(shù)據(jù)類型的抽象,所以蘋果開始聲稱Swift是一門支持面向協(xié)議編程的語言鸳玩。
協(xié)議基礎(chǔ)
官方文檔的定義:
協(xié)議為方法阅虫、屬性、以及其他特定的任務(wù)需求或功能定義藍圖不跟。協(xié)議可被類颓帝、結(jié)構(gòu)體、或枚舉類型采納以提供所需功能的具體實現(xiàn)窝革。滿足了協(xié)議中需求的任意類型都叫做遵循了該協(xié)議购城。
協(xié)議的定義
protocol Food { }
用關(guān)鍵詞 protocol ,聲明一個名為 Food 的協(xié)議虐译。
定義協(xié)議屬性
protocol Pet {
var name: String { get set }
var master: String { get }
static var species: String { get }
}
協(xié)議中定義屬性表示遵循該協(xié)議的類型具備了某種屬性瘪板。
只能使用
var
關(guān)鍵字聲明;需要明確規(guī)定該屬性是可讀的
{get}
漆诽、 還是可讀可寫的{get set}
侮攀;為了保證通用,協(xié)議中必須用static定義類型方法厢拭、類型屬性魏身、類型下標(biāo),因為class只能用在類中蚪腐,不能用于結(jié)構(gòu)體等;
屬性不能賦初始值税朴;
struct Dog: Pet {
var name: String
var master: String
static var species: String = "哺乳動物"
var color: UIColor? = nil
}
var dog = Dog(name: "旺財", master: "小明")
dog.master = "張三" // 更改了主人
定義一個繼承協(xié)議的結(jié)構(gòu)體 Dog 回季,并新增了一個color屬性。
static 修飾的類屬性必須有初始值或?qū)崿F(xiàn)了
get set
方法('static var' declaration requires an initializer expression or getter/setter specifier
)-
set 為什么不報錯正林?
if dog.master == "小明" { dog.master = "張三" }
master 屬性在協(xié)議中被定義為只讀屬性 get泡一,為什么上面的代碼還可以 set ?
協(xié)議中的“只讀”屬性修飾的是協(xié)議這種“類型”的實例觅廓。
let pet: Pet = dog pet.master = "李四"
雖然我們并不能像創(chuàng)建類的實例那樣直接創(chuàng)建協(xié)議的實例鼻忠,但是我們可以通過“賦值”得到一個協(xié)議的實例。此時 就會報錯
Cannot assign to property: 'master' is a get-only property
杈绸。Dog 中新增的 Pet 中沒有的屬性
var color: UIColor? = nil
帖蔓,將不會出現(xiàn)在 pet 中矮瘟。
定義協(xié)議方法
Swift中的協(xié)議可以定義類方法或?qū)嵗椒ǎ谧袷卦搮f(xié)議的類型中塑娇,具體的實現(xiàn)方法的細節(jié)澈侠,通過類或?qū)嵗{(diào)用。
protocol Pet {
var name: String { get set }
var master: String { get }
static var species: String { get }
// 新增的協(xié)議方法
static func sleep()
mutating func changeName()
}
struct Dog: Pet {
var name: String
var master: String
static var species: String = "哺乳動物"
var color: UIColor? = nil
static func sleep() {
print("要休息了")
}
mutating func changeName() {
name = "大黃"
}
}
-
聲明的協(xié)議方法的參數(shù)不能有默認值
? func changeName(name: String = "大黃") // Swift認為默認值也是一種變相的實現(xiàn)
結(jié)構(gòu)體中的方法修改屬性時埋酬,需要在方法前面加上關(guān)鍵字mutating 哨啃,表示該屬性屬性能被修改。這樣的方法叫 異變方法 写妥。
協(xié)議中的初始化器
每一個寵物在被領(lǐng)養(yǎng)的時候拳球,主人就已經(jīng)確定了:
// 在上面的代碼中新增
protocol Pet {
init(master: String)
}
struct Dog: Pet {
init(master: String) {
self.master = master
}
}
此時會報錯 在不初始化所有存儲屬性的情況下從初始化器中返回所有屬性。 ( Return from initializer without initializing all stored properties
)珍特。加上 self.name = ""
就可以了祝峻。
class Cat: Pet {
required init(master: String) {
self.master = master
self.name = ""
}
}
Cat類 遵守了該協(xié)議,初始化器必須用 required 關(guān)鍵字修飾初始化器的具體實現(xiàn)次坡。
繼承與遵守協(xié)議
class SomeClass: NSObject, OneProtocol, TwoProtocol { }
因為Swift中類的繼承是單一的呼猪,但是類可以遵守多個協(xié)議,因此為了突出其單一父類的特殊性砸琅,應(yīng)該 將繼承的父類放在最前面宋距,將遵守的協(xié)議依次放在后面。
多個協(xié)議方法名沖突
protocol ProtocolOne {
func method() -> Int
}
protocol ProtocolTwo {
func method() -> String
}
struct PersonStruct: ProtocolOne, ProtocolTwo {
func method() -> Int {
return 1
}
func method() -> String {
return "Hello World"
}
}
let ps = PersonStruct()
//嘗試調(diào)用返回值為Int的方法
let num = ps.method() ?
//嘗試調(diào)用返回值為String的方法
let string = ps.method() ?
let num = (ps as ProtocolOne).method() ?
let string = (ps as ProtocolTwo).method() ?
編譯器無法知道同名method()
方法到底是哪個協(xié)議中的方法症脂,因此需要指定調(diào)用特定協(xié)議的method()
方法 谚赎。
協(xié)議方法的可選實現(xiàn)
-
方法一:通過 optional 實現(xiàn)可選
@objc protocol OptionalProtocol { @objc optional func optionalMethod() func requiredMethod() }
-
方法二: 通過 extension 做默認處理
protocol OptionalProtocol { func optionalMethod() func requiredMethod() } extension OptionalProtocol { func optionalMethod() { } }
協(xié)議的繼承、聚合
協(xié)議的繼承
協(xié)議可以繼承一個或者多個其他協(xié)議并且可以在它繼承的基礎(chǔ)之上添加更多要求诱篷。協(xié)議繼承的語法與類繼承的語法相似壶唤,選擇列出多個繼承的協(xié)議,使用逗號分隔棕所。
protocol OneProtocol { }
protocol TwoProtocol { }
protocol ThreeProtocol: OneProtocol, TwoProtocol { }
產(chǎn)生了一個 新協(xié)議 闸盔,該協(xié)議擁有 OneProtocol
和 TwoProtocol
的方法或?qū)傩浴P枰獙崿F(xiàn) OneProtocol
和 TwoProtocol
必須的方法或?qū)傩浴?/p>
協(xié)議的聚合
使用形如
OneProtocol & TwoProtocol
的形式實現(xiàn)協(xié)議聚合(組合)復(fù)合多個協(xié)議到一個要求里 琳省。
protocol OneProtocol { }
protocol TwoProtocol { }
typealias FourProtocol = OneProtocol & TwoProtocol
聚合出來的不是新的協(xié)議莫鸭,只是一個代指趟大,代指這些協(xié)議的集合蜒谤。
協(xié)議的繼承和聚合的區(qū)別
首先協(xié)議的繼承是定義了一個全新的協(xié)議赋兵,我們是希望它能夠“大展拳腳”得到普遍使用。而協(xié)議的聚合不一樣桦他,它并沒有定義新的固定協(xié)議類型蔫巩,相反,它只是定義一個臨時的擁有所有聚合中協(xié)議要求組成的局部協(xié)議,很可能是“一次性需求”圆仔,使用協(xié)議的聚合保證了代碼的簡潔性垃瞧、易讀性,同時去除了定義不必要的新類型的繁瑣荧缘,并且定義和使用的地方如此接近皆警,見明知意,也被稱為匿名協(xié)議聚合截粗。但是使用了匿名協(xié)議聚合能夠表達的信息就少了一些信姓,所以需要開發(fā)者斟酌使用。
協(xié)議的檢查
if pig is Pet {
print("遵守了 Pet 協(xié)議")
}
檢查pig 是否是遵守了 Pet 協(xié)議類型的實例绸罗。
協(xié)議的指定
protocol ClassProtocol: class { }
struct Test: ClassProtocol { } // 報錯
使用關(guān)鍵字 class 使定義的協(xié)議只能被類遵守意推。如果有枚舉或結(jié)構(gòu)體嘗試遵守會報錯 Non-class type 'Test' cannot conform to class protocol 'ClassProtocol'
。
協(xié)議作為參數(shù)
func update(param: FourProtocol) { }
將協(xié)議作為參數(shù)珊蟀,表明遵守了該協(xié)議的實例可作為參數(shù)菊值。
協(xié)議的關(guān)聯(lián)類型
協(xié)議的關(guān)聯(lián)類型指的是根據(jù)使用場景的變化,如果協(xié)議中某些屬性存在 邏輯相同的而類型不同 的情況育灸,可以使用關(guān)鍵字associatedtype來為這些屬性的類型聲明“關(guān)聯(lián)類型”腻窒。
protocol LengthMeasurable {
associatedtype LengthType
var length: LengthType { get }
func printMethod()
}
struct Pencil: LengthMeasurable {
typealias LengthType = CGFloat
var length: CGFloat
func printMethod() {
print("鉛筆的長度為 \(length) 厘米")
}
}
struct Bridge: LengthMeasurable {
typealias LengthType = Int
var length: Int
func printMethod() {
print("橋梁的的長度為 \(length) 米")
}
}
LengthMeasurable 協(xié)議中用 associatedtype 定義了一個 類型泛型 。在實現(xiàn)協(xié)議的時候磅崭,定義具體的類型儿子。這樣就可以適配各種物體長度的測量。
associatedtype & typealias的區(qū)別
- associatedtype: 在定義協(xié)議時砸喻,可以用來聲明一個或多個類型作為協(xié)議定義的一部分柔逼,叫關(guān)聯(lián)類型。這種關(guān)聯(lián)類型為協(xié)議中的某個類型提供了自定義名字割岛,其代表的實際類型或?qū)嶋H意義在協(xié)議被實現(xiàn)時才會被指定愉适。
- typealias: 是給 現(xiàn)有 的類型(包括系統(tǒng)和自定義的)進行重新命名,然后就可以用該別名來代替原來的類型癣漆,已達到改善程序可讀性维咸,而且可以自實際編程中根據(jù)業(yè)務(wù)來重新命名,可以表達實際意義惠爽。
協(xié)議的擴展
設(shè)想一個這樣的場景: 有一個人參加比賽癌蓖,三個評委打分。比賽結(jié)束疆股,求這個人的平均分。
protocol Score {
var name: String { get set }
var firstJudge: CGFloat { get set }
var secondJudge: CGFloat { get set }
var thirdJudge: CGFloat { get set }
func averageScore() -> String
}
struct Xiaoming: Score {
var firstJudge: CGFloat
var secondJudge: CGFloat
var thirdJudge: CGFloat
func averageScore() -> String {
let average = (firstJudge + secondJudge + thirdJudge) / 3
return "\(name)的得分為\(average)"
}
}
let xiaoming = Xiaoming(name: "小明", firstJudge: 80, secondJudge: 90, thirdJudge: 100)
let average = xiaoming.averageScore()
這場比賽倒槐,如果有10個人參加旬痹,計算平均值的方法,就需要寫10次。代碼重復(fù)嚴(yán)重两残∮酪悖可以通過 協(xié)議的擴展解決問題 。
extension Score {
func averageScore() -> String {
let average = (firstJudge + secondJudge + thirdJudge) / 3
return "\(name)的得分為\(average)"
}
}
averageScore
默認進行了實現(xiàn)人弓,不需要遵守者必須實現(xiàn)了沼死。
這個時候,比賽承辦方想統(tǒng)計每個人的最高分崔赌,應(yīng)該怎么辦呢意蛀?
extension Score {
func maxScore() -> CGFloat {
return max(firstJudge, secondJudge, thirdJudge)
}
}
let maxScore = xiaoming.maxScore()
比賽承辦方對比賽結(jié)果的輸出不太滿意。想把 小明的得分為90.0
前面統(tǒng)一加上前綴健芭。
// 對系統(tǒng)協(xié)議進行擴展
extension CustomStringConvertible {
var customDescription: String {
return "新希望學(xué)校春季運動會運動會得分為::" + description
}
}
print(xiaoming.averageScore().customDescription)
總結(jié):
- 通過協(xié)議的擴展提供協(xié)議中某些屬性和方法的默認實現(xiàn)县钥。
- 將公共的代碼和屬性統(tǒng)一起來極大的增加了代碼的復(fù)用。
- 為系統(tǒng)/自定義的協(xié)議提供的擴展慈迈。
Swift的55標(biāo)準(zhǔn)庫協(xié)議
Swift的55標(biāo)準(zhǔn)庫協(xié)議可以分為三類
類型 | 描述 | 標(biāo)志 |
---|---|---|
”Can do“協(xié)議 | (表示能力)描述的事情是類型可以做或已經(jīng)做過的若贮。 | 以 -able 結(jié)尾 |
"Is a"協(xié)議 | (表示身份)描述類型是什么樣的,與"Can do"的協(xié)議相比痒留,這些更基于身份谴麦,表示身份的類型。 | 以 -type 結(jié)尾 |
"Can be"協(xié)議 | (表示轉(zhuǎn)換)這個類型可以被轉(zhuǎn)換到或者轉(zhuǎn)換成別的東西伸头。 | 以 -Convertible 結(jié)尾 |
如何更好的命名協(xié)議匾效?
在自定義協(xié)議時應(yīng)該盡可能遵守蘋果的命名規(guī)則,便于開發(fā)人員之間的高效合作熊锭。
55個標(biāo)準(zhǔn)的Swift協(xié)議
55個標(biāo)準(zhǔn)Swift協(xié)議地址(待完成)
協(xié)議編程的優(yōu)勢
面向?qū)ο螅ɡ^承)
有這樣一個需求弧轧,在某個頁面中,顯示的Logo圖片需要切圓角處理碗殷,讓它更美觀一些精绎。
logoImageView.layer.cornerRadius = 5
logoImageView.layer.masksToBounds = true
如果要求,整個APP中所有的Logo都要切圓角處理锌妻。最容易的解決辦法是定義一個名為LogoImageView
的類代乃,使用該類初始化Logo對象。
class LogoImageView: UIImageView {
init(radius: CGFloat = 5) {
super.init(frame: CGRect.zero)
layer.cornerRadius = radius
layer.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
如果要求所有的Logo還要支持點擊抖動效果仿粹。
class LogoImageView: UIImageView {
init(radius: CGFloat = 5) {
super.init(frame: CGRect.zero)
layer.cornerRadius = radius
layer.masksToBounds = true
isUserInteractionEnabled = true
let tap = UITapGestureRecognizer.init(target: self, action: #selector(shakeEvent))
addGestureRecognizer(tap)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension LogoImageView {
@objc func shakeEvent() {
let animation = CAKeyframeAnimation()
animation.keyPath = "transform"
animation.duration = 0.25
let origin = CATransform3DIdentity
let minimum = CATransform3DMakeScale(0.8, 0.8, 1)
let originValue = NSValue(caTransform3D: origin)
let minimumValue = NSValue(caTransform3D:minimum)
animation.values = [originValue, minimumValue, origin, minimumValue, origin]
layer.add(animation, forKey: "bounce")
layer.transform = origin
}
}
這個時候搁吓,如果其他的功能也要使用抖動動畫,就不得不接受切圓角功能吭历。即使把切圓角功能從初始化方法中剝離成一個可選方法堕仔,但是也不得不接受這份耦合代碼。
有的項目里定義了繼承 UIViewController
的父類晌区,實現(xiàn)了很多功能摩骨,項目里頁面都要繼承它通贞。而且往這個自定義UIViewController
里塞代碼實在太方便了,這個類很容易隨著功能迭代逐漸膨脹恼五,變的僵化昌罩,越來越難以維護。下面的子類代碼全都依賴這個父類灾馒,想抽出來復(fù)用非常難茎用。
項目里混合使用了原生功能和H5功能。定義的H5容器的父類WebViewController
睬罗,需要滿足以下業(yè)務(wù)要求:
- 有的H5頁面比較簡單轨功,只需要正常展示網(wǎng)頁即可。
- 有的需要JS代碼注入傅物。
- 有的需要提供保存圖片到相冊給H5使用夯辖。
- 有的需要提供存跳轉(zhuǎn)原始頁面給H5使用。
這個父類中處理了WKWebView
實現(xiàn)董饰、 JS注入蒿褂、橋的定義以及橋功能的實現(xiàn)等眾多能力。導(dǎo)致WebViewController
代碼量多達幾千行卒暂。很難維護擴展啄栓。
采用 繼承 方式解決復(fù)用的問題,很容易帶來代碼的耦合也祠。
假如 UILabel 也需要抖動的動畫昙楚,采用繼承無法實現(xiàn)。UIImageView 和 UILabel 已經(jīng)是 UIView的子類诈嘿,除非改動UIView堪旧,否則無法通過繼承的方式實現(xiàn)。
面向?qū)ο螅〝U展)
通過擴展奖亚,可以不修改類的實現(xiàn)文件的情況下淳梦,給類增加新的方法∥糇郑可以通過給 UIView 添加擴展來解決 UIImageView 和 UILabel 同時增加抖動功能的需求爆袍。缺點是給一個類加上這個東西就污染了該類所有的對象。(UIButton說: 我不需要為什么塞給我作郭?)
面向?qū)ο螅üぞ哳悾?/h3>
當(dāng)然陨囊,我們可以直接寫一個工具類來實現(xiàn)這個抖動的效果,然后把必要的參數(shù)(layer)傳遞過來夹攒。缺點就是蜘醋,使用起來相對麻煩,眾多的工具類難易管理咏尝。(你有因為不知道該使用哪個工具類頭疼過么压语? 有因為要把方法放哪個工具類頭疼過么闲先?有因為工具類代碼量過多頭疼過么?)
面向協(xié)議
通過協(xié)議重新實現(xiàn) 切圓角功能和抖動動畫功能无蜂。
/// 聲明一個圓角的能力協(xié)議
protocol RoundCornerable {
func roundCorner(radius: CGFloat)
}
/// 通過擴展給這個協(xié)議方法添加默認實現(xiàn),必須滿足遵守這個協(xié)議的類是繼承UIView的蒙谓。
extension RoundCornerable where Self: UIView {
func roundCorner(radius: CGFloat) {
layer.cornerRadius = radius
layer.masksToBounds = true
}
}
/// 聲明抖動動畫的協(xié)議
protocol Shakeable {
func startShake()
}
/// 實現(xiàn)協(xié)議方法內(nèi)容斥季,并指定只有LogoImageView才可以使用。
extension Shakeable where Self: LogoImageView {
func startShake() {
let animation = CAKeyframeAnimation()
animation.keyPath = "transform"
animation.duration = 0.25
let origin = CATransform3DIdentity
let minimum = CATransform3DMakeScale(0.8, 0.8, 1)
let originValue = NSValue(caTransform3D: origin)
let minimumValue = NSValue(caTransform3D:minimum)
animation.values = [originValue, minimumValue, origin, minimumValue, origin]
layer.add(animation, forKey: "bounce")
layer.transform = origin
}
}
/// 遵守了RoundCornerable協(xié)議累驮,才擁有切圓角的功能酣倾。遵守了Shakeable協(xié)議,才擁有抖動動畫效果谤专。
class LogoImageView: UIImageView, RoundCornerable, Shakeable {
init(radius: CGFloat = 5) {
super.init(frame: CGRect.zero)
roundCorner(radius: radius)
isUserInteractionEnabled = true
let tap = UITapGestureRecognizer.init(target: self, action: #selector(shakeEvent))
addGestureRecognizer(tap)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func shakeEvent() {
startShake()
}
}
泛型及泛型約束
有時候會有一些場景聲明的協(xié)議只給部分對象使用躁锡。
/// 只給遵守Shakeable協(xié)議的LogoImageView添加了拓展
extension Shakeable where Self: LogoImageView { }
協(xié)議解決面向?qū)ο笾屑值某悊栴}
該模塊引用于:Swift標(biāo)準(zhǔn)庫中常見的協(xié)議。 由于示例過于優(yōu)秀置侍,故直接引用映之。
麻雀
作為一種鳥類,應(yīng)該繼承鳥
蜡坊,但是如果繼承了鳥
杠输,就相當(dāng)于默認了麻雀
是一種寵物
,這顯然是不和邏輯的秕衙。麻雀
在圖中的位置就顯得比較尷尬蠢甲。解決此問題的一般方法如下乍一看好像解決了這樣的問題,但是仔細想由于Swift只支持單繼承据忘,
麻雀
沒有繼承鳥
類就無法體現(xiàn)麻雀
作為一種鳥擁有的特性(比如飛翔)鹦牛。如果此時出現(xiàn)一個新的飛機
類,雖然飛機
和寵物
之間沒有任何聯(lián)系勇吊,但是飛機
和鳥
是由很多共同特性的(比如飛翔)曼追,這樣的特性該如何體現(xiàn)呢?答案還是新建一個類成為動物
和飛機
的父類萧福。
面向?qū)ο缶褪沁@樣一層一層的向上新建父類最終得到一個“超級父類”NSObject
拉鹃。盡管問題得到了解決,但是麻雀
與鳥
鲫忍、飛機
與鳥
之間的共性并沒有得到很好的體現(xiàn)膏燕。而協(xié)議的出現(xiàn)正是為了解決這類問題。
實際上圖中包括
動物
悟民、鳥
坝辫、飛機
等類之間的關(guān)系就應(yīng)該是如上圖所示的繼承關(guān)系。使用協(xié)議將“寵物”射亏、“飛翔”等看作是一種特性近忙,或者是從另一個維度描述這種類別竭业,更重要的是使用協(xié)議并不會打破原有類別之間繼承的父子關(guān)系。
和飛翔相關(guān)的代碼統(tǒng)一放在Flyable
中及舍,需要“飛翔”這種能力就遵守該協(xié)議未辆;和寵物相關(guān)的代碼統(tǒng)一放在PetType
中,需要成為寵物就遵守該協(xié)議锯玛。這些
協(xié)議靈活多變咐柜,結(jié)合原有的面向?qū)ο箢愔g固有的繼承關(guān)系,完美的描述了這個世界攘残。