面向協(xié)議編程并非銀彈
銀彈(Silver Bullet)一詞出自IBM大型機(jī)之父Frederick P. Brooks Jr.在1986年發(fā)表的一篇關(guān)于軟件工程的經(jīng)典論文《沒有銀彈:軟件工程的本質(zhì)性與附屬性工作》(No Silver Bullet — Essence and Accidents of Software Engineering)。其中的“銀彈”是指一項(xiàng)可使軟件工程的生產(chǎn)力在十年內(nèi)提高十倍的技術(shù)或方法。該論文強(qiáng)調(diào)由于軟件的復(fù)雜性本質(zhì),而使這樣“真正的銀彈”并不存在。
銀彈在軟件工程中的含義是指妄圖創(chuàng)造某種便捷的開發(fā)技術(shù),從而使某個(gè)項(xiàng)目的實(shí)施提高效率。又或者指擺脫該項(xiàng)目的本質(zhì)或核心,而達(dá)到超乎想象的成功。但這么做的結(jié)果卻是徒勞的。
在本文中Chris介紹了Swift中的面向協(xié)議編程的濫用情況,認(rèn)為很多時(shí)候有更簡(jiǎn)單的解決辦法,面向協(xié)議編程并非銀彈。
以下為正文:
在Swift語言中,面向協(xié)議編程很流行。在“面向協(xié)議”那兒有很多Swift代碼,一些開源庫甚至將其聲明為功能。我覺得協(xié)議在Swift中被過度濫用了,其實(shí)問題常??梢杂酶?jiǎn)單的方式來解決。簡(jiǎn)而言之,就是不要生搬硬套協(xié)議的條條框框,而不知變通。
在WWDC2015上蘋果推出了一個(gè)Session叫“Swift中的面向協(xié)議編程”,它成了這屆大會(huì)上最有影響力的Session之一。它表明了除某些情況外,用戶可以使用面向協(xié)議的解決方案(即協(xié)議和一些符合協(xié)議的類型)來替換類層次結(jié)構(gòu)(即超類和一些子類)。面向協(xié)議的解決方案更簡(jiǎn)單、更靈活。例如,一個(gè)類只能有一個(gè)超類,但一個(gè)類型可以符合許多協(xié)議。
讓我們來看看他們?cè)赪WDC演講中解決的這個(gè)問題:一系列繪圖命令需要渲染成圖像,并將指令記錄到控制臺(tái)。通過將繪圖命令放在協(xié)議中,描述繪圖的任何代碼可以根據(jù)協(xié)議的方法來表述。協(xié)議擴(kuò)展允許你根據(jù)協(xié)議的基本功能定義新的繪圖功能,并且每個(gè)符合的類型都可以自動(dòng)獲得新的功能。
在上述例子中,協(xié)議解決了多種類型之間共享代碼的問題。在Swift的標(biāo)準(zhǔn)庫中,協(xié)議主要用于Collection類型,用來解決完全相同的問題。因?yàn)閐ropFirst用Collection類型定義,所有的Collection類型都能自動(dòng)得到它。與此同時(shí),標(biāo)準(zhǔn)庫中定義了太多的Collection相關(guān)的協(xié)議和類型,當(dāng)我們想找東西時(shí)會(huì)面臨困難。這是協(xié)議的一個(gè)缺點(diǎn),然而,在標(biāo)準(zhǔn)庫的情況下還是利大于弊。
現(xiàn)在,讓我們通過一個(gè)例子來開始。這里有一個(gè)WebService類。它使用URLSession從網(wǎng)絡(luò)加載實(shí)體。(實(shí)際上并不加載東西,領(lǐng)會(huì)意思即可):
- 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
- }
- }
上面的代碼很短,運(yùn)行正常。直到我們要測(cè)試loadUser和loadEpisode之前,沒有什么問題?,F(xiàn)在我們要么用stub方法來模擬load,要么通過依賴注入來傳入一個(gè)模擬的URLSession。我們還可以定義一個(gè)符合URLSession的協(xié)議,然后傳遞一個(gè)測(cè)試實(shí)例。不過在這個(gè)案例中,我們采用更簡(jiǎn)單的解決方案,將Webservice更改的部分取出并轉(zhuǎn)換為結(jié)構(gòu)體:
- 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)在,我們可以不必通過模擬任何東西來測(cè)試user和episode了:它們是簡(jiǎn)單的結(jié)構(gòu)值。我們?nèi)匀恍枰獪y(cè)試load,但只有這一個(gè)方法需要寫測(cè)試(而不是為每個(gè)資源)?,F(xiàn)在讓我們來添加一些協(xié)議。
取代parse函數(shù),我們可以為能夠從JSON初始化的類型創(chuàng)建一個(gè)協(xié)議。
- 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)
- }
- }
上面的代碼可能看起來更簡(jiǎn)單,但靈活性也大大降低。例如,你如何定義一個(gè)具有User值的數(shù)組資源?(上述面向協(xié)議的例子中,是不可能實(shí)現(xiàn)的,我們必須等待Swift 4或5,直至可用。)協(xié)議使代碼得以簡(jiǎn)化,但我認(rèn)為它不為自身買單,因?yàn)樗蟠鬁p少了我們可以創(chuàng)建一個(gè)Resource的方式。
代替將user和episode作為Resource值,我們還可以使Resource成為協(xié)議并具有UserResource和EpisodeResource結(jié)構(gòu)。這似乎是一個(gè)很流行的做法,因?yàn)閾碛蓄愋捅戎皇且粋€(gè)值來說,“就是感覺要對(duì)一些”:
- 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)
- }
- }
但如果我們仔細(xì)看看,我們真正得到了什么?代碼變得更冗長、復(fù)雜、不直觀。并且由于關(guān)聯(lián)類型,結(jié)果最后我們可能定義一個(gè)AnyResource。EpisodeResource結(jié)構(gòu)和episodeResource值有什么區(qū)別呢?它們都是全局定義的。對(duì)于結(jié)構(gòu)體,名稱以大寫字母開頭;而對(duì)于值,則使用小寫字母。除此之外,結(jié)構(gòu)真的沒有任何優(yōu)勢(shì)。你可以將它們加入命名空間(自動(dòng)補(bǔ)全)。所以在這種情況下,有一個(gè)值肯定會(huì)更簡(jiǎn)短。
我在網(wǎng)上看到的很多代碼例子。例如,我看到這樣的協(xié)議:
- protocol URLStringConvertible {
- var urlString: String { get }
- }
- // Somewhere laterfunc sendRequest(urlString: URLStringConvertible, method: ...) {
- let string = urlString.urlString
- }
是什么打動(dòng)了你?為什么不簡(jiǎn)單地刪除協(xié)議并直接傳遞urlString呢?這樣就簡(jiǎn)單多了。或者,一個(gè)單一方法的協(xié)議:
- protocol RequestAdapter {
- func adapt(_ urlRequest: URLRequest) throws -> URLRequest}
有些爭(zhēng)議的是:為什么不簡(jiǎn)單地刪除協(xié)議,并在某處傳遞函數(shù)?這樣豈不是更簡(jiǎn)單。(除非你的協(xié)議是一個(gè)類的協(xié)議,你想要一個(gè)弱引用)。
我可以繼續(xù)展示例子,但我希望希望你已經(jīng)明確我的觀點(diǎn):多數(shù)情況下都有更簡(jiǎn)單的選擇。更抽象地說,協(xié)議只是實(shí)現(xiàn)多態(tài)代碼的一種方式。還有許多其他方法:子類、泛型、值、函數(shù)等。使用值(例如,一個(gè)String,而不是一個(gè)URLStringConvertible)是最簡(jiǎn)單的方法。函數(shù)(例如adapt而不是RequestAdapter)比值復(fù)雜一點(diǎn),但仍然很簡(jiǎn)單。泛型(無任何限制)比協(xié)議簡(jiǎn)單。為了完成代碼,協(xié)議通常比類層次結(jié)構(gòu)更簡(jiǎn)單。
一個(gè)有用的啟發(fā)是,也許是考慮您的協(xié)議是依照數(shù)據(jù)還是行為來建模。對(duì)于數(shù)據(jù),結(jié)構(gòu)可能更容易。對(duì)于復(fù)雜的行為(例如,具有多個(gè)方法的委托),協(xié)議通常更容易。(標(biāo)準(zhǔn)庫的collection協(xié)議有點(diǎn)特別:它們并不真正描述數(shù)據(jù),而是描述數(shù)據(jù)操作。)
也就是說,協(xié)議可能非常有用。但不要為了面向協(xié)議編程而編程。首先要審視你的問題,并嘗試以最簡(jiǎn)單的方式來解決它。讓問題推動(dòng)解決方案,而不是相反。面向協(xié)議編程本身無所謂好與壞。就像任何其他技術(shù)(函數(shù)式編程,OO,依賴注入,子類化)一樣,它可以用來解決一個(gè)問題,我們應(yīng)該嘗試選擇合適的工具。有時(shí)這是一個(gè)協(xié)議,但往往,有一個(gè)更簡(jiǎn)單的方法。
其他
- Beyond Crusty:Real-World Protocols: http://www.thedotpost.com/2016/01/rob-napier-beyond-crusty-real-world-protocols
- Haskell Game Object Design - Or How Functions Can Get You Apples: http://www.gamedev.net/page/resources/_/technical/game-programming/haskell-game-object-design-or-how-functions-can-get-you-apples-r3204