面向協(xié)議編程并非銀彈

前言

本文翻譯自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è)試loadUserloadEpisode的時(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 1Advanced 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)行userepisode的測(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é)議的UserResourceEpisodeResource結(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蒂窒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子荞怒,更是在濱河造成了極大的恐慌洒琢,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褐桌,死亡現(xiàn)場(chǎng)離奇詭異衰抑,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)荧嵌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門呛踊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人啦撮,你說我怎么就攤上這事谭网。” “怎么了赃春?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵愉择,是天一觀的道長。 經(jīng)常有香客問我,道長薄辅,這世上最難降的妖魔是什么要拂? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮站楚,結(jié)果婚禮上脱惰,老公的妹妹穿的比我還像新娘。我一直安慰自己窿春,他們只是感情好拉一,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布旧乞。 她就那樣靜靜地躺著蔚润,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尺栖。 梳的紋絲不亂的頭發(fā)上嫡纠,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音延赌,去河邊找鬼除盏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛挫以,可吹牛的內(nèi)容都是我干的者蠕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼掐松,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼踱侣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起大磺,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤抡句,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后杠愧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體待榔,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年殴蹄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猾担。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡袭灯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绑嘹,到底是詐尸還是另有隱情稽荧,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布工腋,位于F島的核電站姨丈,受9級(jí)特大地震影響畅卓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蟋恬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一翁潘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧歼争,春花似錦拜马、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至乔遮,卻和暖如春扮超,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蹋肮。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工出刷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人括尸。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓巷蚪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親濒翻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子屁柏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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

  • 第一章.面向?qū)ο笈c面向協(xié)議編程 本書是關(guān)于面向協(xié)議編程。當(dāng)蘋果2015年的開發(fā)者大會(huì)上發(fā)布了Swift2有送,他們也宣...
    醬油不愛醋閱讀 1,373評(píng)論 0 7
  • 第一章 面向?qū)ο缶幊毯兔嫦騾f(xié)議編程 這本書是關(guān)于面向協(xié)議編程的淌喻。當(dāng)蘋果在 2015 年世界開發(fā)者大會(huì)上宣布 Swi...
    焉知非魚閱讀 4,976評(píng)論 19 25
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件雀摘、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,022評(píng)論 4 62
  • //創(chuàng)建百度編輯器 {$detail['condition']} UE.getEditor('textarea1...
    無名程序員11閱讀 504評(píng)論 0 0
  • 這么一座普通的小院子裸删,別樣溫暖。 午后阵赠,陽光懶懶地斜靠著低矮的房子涯塔,悠悠灑在院子里。風(fēng)拂過清蚀,夾著樹葉清苦的味道匕荸,亦...
    溱子閱讀 403評(píng)論 0 6