面向協議編程并非銀彈
銀彈(Silver Bullet)一詞出自IBM大型機之父Frederick P. Brooks Jr.在1986年發表的一篇關于軟件工程的經典論文《沒有銀彈:軟件工程的本質性與附屬性工作》(No Silver Bullet — Essence and Accidents of Software Engineering)。其中的“銀彈”是指一項可使軟件工程的生產力在十年內提高十倍的技術或方法。該論文強調由于軟件的復雜性本質,而使這樣“真正的銀彈”并不存在。
銀彈在軟件工程中的含義是指妄圖創造某種便捷的開發技術,從而使某個項目的實施提高效率。又或者指擺脫該項目的本質或核心,而達到超乎想象的成功。但這么做的結果卻是徒勞的。
在本文中Chris介紹了Swift中的面向協議編程的濫用情況,認為很多時候有更簡單的解決辦法,面向協議編程并非銀彈。
以下為正文:
在Swift語言中,面向協議編程很流行。在“面向協議”那兒有很多Swift代碼,一些開源庫甚至將其聲明為功能。我覺得協議在Swift中被過度濫用了,其實問題常常可以用更簡單的方式來解決。簡而言之,就是不要生搬硬套協議的條條框框,而不知變通。
在WWDC2015上蘋果推出了一個Session叫“Swift中的面向協議編程”,它成了這屆大會上最有影響力的Session之一。它表明了除某些情況外,用戶可以使用面向協議的解決方案(即協議和一些符合協議的類型)來替換類層次結構(即超類和一些子類)。面向協議的解決方案更簡單、更靈活。例如,一個類只能有一個超類,但一個類型可以符合許多協議。
讓我們來看看他們在WWDC演講中解決的這個問題:一系列繪圖命令需要渲染成圖像,并將指令記錄到控制臺。通過將繪圖命令放在協議中,描述繪圖的任何代碼可以根據協議的方法來表述。協議擴展允許你根據協議的基本功能定義新的繪圖功能,并且每個符合的類型都可以自動獲得新的功能。
在上述例子中,協議解決了多種類型之間共享代碼的問題。在Swift的標準庫中,協議主要用于Collection類型,用來解決完全相同的問題。因為dropFirst用Collection類型定義,所有的Collection類型都能自動得到它。與此同時,標準庫中定義了太多的Collection相關的協議和類型,當我們想找東西時會面臨困難。這是協議的一個缺點,然而,在標準庫的情況下還是利大于弊。
現在,讓我們通過一個例子來開始。這里有一個WebService類。它使用URLSession從網絡加載實體。(實際上并不加載東西,領會意思即可):
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 } }
上面的代碼很短,運行正常。直到我們要測試loadUser和loadEpisode之前,沒有什么問題。現在我們要么用stub方法來模擬load,要么通過依賴注入來傳入一個模擬的URLSession。我們還可以定義一個符合URLSession的協議,然后傳遞一個測試實例。不過在這個案例中,我們采用更簡單的解決方案,將Webservice更改的部分取出并轉換為結構體:
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) } }
現在,我們可以不必通過模擬任何東西來測試user和episode了:它們是簡單的結構值。我們仍然需要測試load,但只有這一個方法需要寫測試(而不是為每個資源)。現在讓我們來添加一些協議。
取代parse函數,我們可以為能夠從JSON初始化的類型創建一個協議。
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) } }
上面的代碼可能看起來更簡單,但靈活性也大大降低。例如,你如何定義一個具有User值的數組資源?(上述面向協議的例子中,是不可能實現的,我們必須等待Swift 4或5,直至可用。)協議使代碼得以簡化,但我認為它不為自身買單,因為它大大減少了我們可以創建一個Resource的方式。
代替將user和episode作為Resource值,我們還可以使Resource成為協議并具有UserResource和EpisodeResource結構。這似乎是一個很流行的做法,因為擁有類型比只是一個值來說,“就是感覺要對一些”:
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) } }
但如果我們仔細看看,我們真正得到了什么?代碼變得更冗長、復雜、不直觀。并且由于關聯類型,結果最后我們可能定義一個AnyResource。EpisodeResource結構和episodeResource值有什么區別呢?它們都是全局定義的。對于結構體,名稱以大寫字母開頭;而對于值,則使用小寫字母。除此之外,結構真的沒有任何優勢。你可以將它們加入命名空間(自動補全)。所以在這種情況下,有一個值肯定會更簡短。
我在網上看到的很多代碼例子。例如,我看到這樣的協議:
protocol URLStringConvertible { var urlString: String { get } } // Somewhere laterfunc sendRequest(urlString: URLStringConvertible, method: ...) { let string = urlString.urlString }
是什么打動了你?為什么不簡單地刪除協議并直接傳遞urlString呢?這樣就簡單多了。或者,一個單一方法的協議:
protocol RequestAdapter { func adapt(_ urlRequest: URLRequest) throws -> URLRequest}
有些爭議的是:為什么不簡單地刪除協議,并在某處傳遞函數?這樣豈不是更簡單。(除非你的協議是一個類的協議,你想要一個弱引用)。
我可以繼續展示例子,但我希望希望你已經明確我的觀點:多數情況下都有更簡單的選擇。更抽象地說,協議只是實現多態代碼的一種方式。還有許多其他方法:子類、泛型、值、函數等。使用值(例如,一個String,而不是一個URLStringConvertible)是最簡單的方法。函數(例如adapt而不是RequestAdapter)比值復雜一點,但仍然很簡單。泛型(無任何限制)比協議簡單。為了完成代碼,協議通常比類層次結構更簡單。
一個有用的啟發是,也許是考慮您的協議是依照數據還是行為來建模。對于數據,結構可能更容易。對于復雜的行為(例如,具有多個方法的委托),協議通常更容易。(標準庫的collection協議有點特別:它們并不真正描述數據,而是描述數據操作。)
也就是說,協議可能非常有用。但不要為了面向協議編程而編程。首先要審視你的問題,并嘗試以最簡單的方式來解決它。讓問題推動解決方案,而不是相反。面向協議編程本身無所謂好與壞。就像任何其他技術(函數式編程,OO,依賴注入,子類化)一樣,它可以用來解決一個問題,我們應該嘗試選擇合適的工具。有時這是一個協議,但往往,有一個更簡單的方法。
其他
- 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
來自:http://mobile.51cto.com/app-show-524738.htm