前言
本文翻譯自Protocol Oriented Programming is Not a Silver Bullet
翻譯的不對(duì)的地方還請(qǐng)多多包涵指正,謝謝~
面向協(xié)議編程并非銀彈
為什么我們應(yīng)該建設(shè)性地使用協(xié)議 (be critical of using protocols)
在Swift中忍抽,面向協(xié)議編程非常流行。有許多代碼都是面向協(xié)議的银萍,一些開源的庫甚至聲明它是庫的一個(gè)特性络凿。我認(rèn)為協(xié)議在Swift中被過度使用了憨攒,有些問題可以簡單的方式解決觉既。簡言之:不要教條式的使用(或避免)使用協(xié)議惧盹。
在2015 WWDC上最具影響力的章節(jié)之一是Protocol-Oriented Programming in Swift。它闡述了你可以用面向協(xié)議方案(就是說瞪讼,遵循協(xié)議的協(xié)議或者類型)替代類繼承(就是說榄审,父類或者子類)甫题。面向協(xié)議的方案更加簡單癌压,更加靈活衙荐。例如,類只能有一個(gè)父類希柿,但類型可以遵循多個(gè)協(xié)議秒际。
讓我們來看看他們?cè)赪WDC演講上說的問題。一些列的繪制命令需要作為圖形被繪制狡汉,且需要輸出到控制臺(tái)。通過在協(xié)議內(nèi)定義繪制命令闽颇,描述繪制的任何代碼都可以被協(xié)議的方法進(jìn)行解析盾戴。協(xié)議擴(kuò)展能夠允許你定義新的繪制功能,作為協(xié)議的基礎(chǔ)功能兵多,那么任意遵循該協(xié)議的類型都能免費(fèi)的獲得這個(gè)功能尖啡。
在以上例子中,協(xié)議解決了在多個(gè)類型間共享代碼的問題剩膘。在Swift標(biāo)準(zhǔn)庫中衅斩,協(xié)議重度地使用在集合中,他們解決也是相同的問題怠褐。因?yàn)?code>Collection類型定義dropFirst
方法畏梆,所有集合類型都免費(fèi)的獲得了這個(gè)方法~ 同時(shí),有許多集合相關(guān)的類型和協(xié)議奈懒,找起來很困難奠涌。這就是協(xié)議其中一個(gè)缺點(diǎn),但在標(biāo)準(zhǔn)庫這個(gè)例子中協(xié)議的優(yōu)勢(shì)還是大于它的這個(gè)劣勢(shì)磷杏。
現(xiàn)在溜畅,讓我們通過一個(gè)例子來說明。這里极祸,我們有一個(gè)Webservice
的類慈格。它使用URLSession
從網(wǎng)絡(luò)上下載實(shí)體怠晴。(實(shí)際上它并沒有下載東西,僅用于說明)
class Webservice {
func loadUser() -> User? {
let json = self.load(URL(string: "/users/current")!)
return User(json: json)
}
func loadEpisode() -> Episode? {
let json = self.load(URL(string: "/episodes/latest")!)
return Episode(json: json)
}
private func load(_ url: URL) -> [AnyHashable:Any] {
URLSession.shared.dataTask(with: url)
// etc.
return [:] // should come from the server
}
}
上述代碼簡單并工作地很好浴捆。它沒有問題蒜田,直到我們希望測(cè)試loadUser
和loadEpisode
的時(shí)候。現(xiàn)在我們要不存根加載汤功,或者使用依賴注入的方式傳一個(gè)模擬的請(qǐng)求進(jìn)去物邑。我們可以定義一個(gè)URLSession
遵循的請(qǐng)求并在一個(gè)測(cè)試實(shí)例中傳遞進(jìn)去。但是滔金,在這個(gè)例子中色解,解決辦法可以更簡單:我們可以將需要改變的部分從Webservice
抽離到一個(gè)結(jié)構(gòu)體中(在Swift Talk Episode 1
及Advanced Swift
也介紹過):
struct Resource<A> {
let url: URL
let parse: ([AnyHashable:Any]) -> A
}
class Webservice {
let user = Resource<User>(url: URL(string: "/users/current")!, parse: User.init)
let episode = Resource<Episode>(url: URL(string: "/episodes/latest")!, parse: Episode.init)
private func load<A>(resource: Resource<A>) -> A {
URLSession.shared.dataTask(with: resource.url)
// load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result.
let json: [AnyHashable:Any] = [:] // should come from the server
return resource.parse(json)
}
}
現(xiàn)在,我們可以不通過模擬任何東西進(jìn)行user
和episode
的測(cè)試:他們是簡單的結(jié)構(gòu)體類型餐茵。我們?nèi)匀徊坏貌粶y(cè)試load
方法科阎,但那僅僅是一個(gè)方法(針對(duì)對(duì)每個(gè)資源的)。現(xiàn)在讓我們來添加一些協(xié)議忿族。
我們可以為類型定義一個(gè)能從JSON數(shù)據(jù)初始化的協(xié)議锣笨,而不是一個(gè)parse
函數(shù)。
protocol FromJSON {
init(json: [AnyHashable:Any])
}
struct Resource<A: FromJSON> {
let url: URL
}
class Webservice {
let user = Resource<User>(url: URL(string: "/users/current")!)
let episode = Resource<Episode>(url: URL(string: "/episodes/latest")!)
private func load<A>(resource: Resource<A>) -> A {
URLSession.shared.dataTask(with: resource.url)
// load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result.
let json: [AnyHashable:Any] = [:] // should come from the server
return A(json: json)
}
}
上述代碼可能看起來更簡潔道批,但它也缺少一些靈活性错英。例如,你怎樣定義一個(gè)擁有User
值數(shù)組的資源(Resource
)呢隆豹?(在上述面向協(xié)議的例子中椭岩,還不可能,我們不得不等到Swift4或者5時(shí)才能看到)璃赡。協(xié)議可以使事情更加簡單判哥,但我認(rèn)為這不能為其缺點(diǎn)買賬,因?yàn)樗鼧O大地創(chuàng)建Resource
的方式碉考。
我們可以將Resource
作為協(xié)議并創(chuàng)建遵循該協(xié)議的UserResource
和EpisodeResource
結(jié)構(gòu)體塌计,代替將user, episode
作為Resource
的值類型。這看起來是非常普遍的做法侯谁,因?yàn)閾碛幸粋€(gè)類型而不是一個(gè)值“感覺很合適”锌仅。
protocol Resource {
associatedtype Result
var url: URL { get }
func parse(json: [AnyHashable:Any]) -> Result
}
struct UserResource: Resource {
let url = URL(string: "/users/current")!
func parse(json: [AnyHashable : Any]) -> User {
return User(json: json)
}
}
struct EpisodeResource: Resource {
let url = URL(string: "/episodes/latest")!
func parse(json: [AnyHashable : Any]) -> Episode {
return Episode(json: json)
}
}
class Webservice {
private func load<R: Resource>(resource: R) -> R.Result {
URLSession.shared.dataTask(with: resource.url)
// load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result.
let json: [AnyHashable:Any] = [:]
return resource.parse(json: json)
}
}
但如果我們嚴(yán)格地審視,我們真正有獲得了什么墙贱?代碼變長技扼,更加復(fù)雜且不直接。并且因?yàn)殛P(guān)聯(lián)對(duì)象嫩痰,最終我們很可能一個(gè)AnyResource
剿吻。使用EpisodeResource
結(jié)構(gòu)體而不是episodeResource
值真的有好處嗎?他們都是全局定義的串纺。對(duì)于結(jié)構(gòu)體丽旅,名字是以大寫字母開頭椰棘,對(duì)于值類型,是小寫字母榄笙。除此之外邪狞,幾乎沒有任何優(yōu)點(diǎn)。他們都可以有命名空間(對(duì)于自動(dòng)補(bǔ)全來說)茅撞。因此對(duì)于這個(gè)例子帆卓,使用值絕對(duì)是更簡單,短小米丘。
在圍繞網(wǎng)絡(luò)方面的代碼中剑令,有許多其他的例子。例如拄查,我看到這樣一個(gè)協(xié)議:
protocol URLStringConvertible {
var urlString: String { get }
}
// Somewhere later
func sendRequest(urlString: URLStringConvertible, method: ...) {
let string = urlString.urlString
}
這對(duì)你來說有什么好處呢吁津?為什么不去掉協(xié)議直接傳進(jìn)urlString
來呢?更簡單的堕扶,看這樣有單個(gè)方法的協(xié)議:
protocol RequestAdapter {
func adapt(_ urlRequest: URLRequest) throws -> URLRequest
}
更為有爭議的是:為什么不簡單地去掉協(xié)議碍脏,在某處傳遞一個(gè)方法?更簡單吧稍算。(除非你的協(xié)議是一個(gè)僅適用于類的協(xié)議典尾,且你希望若引用它)。
我可以繼續(xù)舉例子糊探,但我希望觀點(diǎn)已經(jīng)非常清晰了急黎。大多數(shù)來說,有更加簡單的選擇侧到。更加抽象地說,協(xié)議僅僅是一種實(shí)現(xiàn)多態(tài)代碼的方式淤击。有許多其他的方法:子類匠抗,泛型,值污抬,函數(shù)等等汞贸。值(例如,一個(gè)字符串而不是一個(gè)URLStringConvertible
協(xié)議)是最簡單的方式印机。函數(shù)(直接采用而不是RequestAdapter
的協(xié)議)比值更加復(fù)雜一些矢腻,但仍然簡單。泛型(沒有任何限制)比協(xié)議更加簡單射赛。為完成某件事多柑,協(xié)議相對(duì)類的層次來說通常更更加簡單。
一個(gè)更具啟發(fā)式方法可能是思考你的協(xié)議是對(duì)數(shù)據(jù)還是行為建模楣责。對(duì)于數(shù)據(jù)竣灌,結(jié)構(gòu)體可能更加簡單聂沙。對(duì)于行為動(dòng)作(比如:有很多方法的代理),協(xié)議通常更加簡單初嘹。(標(biāo)準(zhǔn)庫中的結(jié)合協(xié)議有點(diǎn)特殊:他們實(shí)際不是描述數(shù)據(jù)及汉,而不是數(shù)據(jù)操作。)
也就是說屯烦,協(xié)議可以非常有用坷随。但不要僅僅因?yàn)樾枰嫦騾f(xié)議編程而先開始寫協(xié)議。應(yīng)該先審視你的問題驻龟,盡可能地用最簡單的方式來解決它温眉。讓問題來驅(qū)動(dòng)解決方案,而不是其他因素迅脐。面向協(xié)議編程本性并不是好或者壞芍殖。就像其他技術(shù)一樣(函數(shù)式編程,面向?qū)ο笄疵铮蕾囎⑷胪憧ィ宇惢┦怯脕斫鉀Q問題,我們應(yīng)當(dāng)選擇合適的工具進(jìn)行工作隐锭。有時(shí)它是協(xié)議編程窃躲,但通常,有更簡單的方案钦睡。
想了解更多:
Beyond Crusty: Real-World Protocols
Haskell Game Object Design - Or How Functions Can Get You Apples