做者:Erica Sadun,原文連接,原文日期:2016-01-01
譯者:walkingway;校對:saitjr;定稿:shanksios
今天,iOS Dev 週刊 貼出一篇 Alexei Kuznetsov 的關於『從你的代碼中刪除 guard
』的文章。Kuznetsov 指出支持他這篇文章的理論依據主要來自於 Robert C. Martin,這位世界頂級軟件開發大師提出:代碼必須精簡。即關於函數存在兩條規則,第一條:函數應該保持精簡;第二條:沒有最精簡,只有更精簡。Alexei Kuznetsov 表示應將 Martin 的理論應用在從此的 Swift 開發中。swift
Kuznetsov 寫到『使用 guard
語句能有效減小函數中的嵌套數量,但 guard
存在一些問題。使用 guard
語句會使咱們在一個函數中作更多的事情,以及維護多個級別的抽象。若是咱們堅持短小、功能單一的函數,就會發現根本不須要 guard
』。安全
我寫這篇文章的目的是爲了反駁 Kuznetsov 提出的觀點,接下來我要說說個人見解。ide
下面的代碼片斷來自於蘋果官方《Swift Programming Language》書中的示例,他設計了一個虛擬的自動販賣機。 vend
函數實現了『顧客成功付款後,將商品分發到消費者手中』的功能。若是我沒數錯的話,官方提供的原始函數一共是 18 行代碼(25 ~ 42 行),這個數量包括三條 guard
語句,四條執行語句,以及他們之間的換行符。函數
struct Item { var price: Int var count: Int } enum VendingMachineError: ErrorType { case InvalidSelection case InsufficientFunds(coinsNeeded: Int) case OutOfStock } class VendingMachine { var inventory = [ "Candy Bar": Item(price: 12, count: 7), "Chips": Item(price: 10, count: 4), "Pretzels": Item(price: 7, count: 11) ] var coinsDeposited = 0 func dispense(snack: String) { print("Dispensing \(snack)") } func vend(itemNamed name: String) throws { guard var item = inventory[name] else { throw VendingMachineError.InvalidSelection } guard item.count > 0 else { throw VendingMachineError.OutOfStock } guard item.price <= coinsDeposited else { throw VendingMachineError.InsufficientFunds(coinsNeeded: item.price - coinsDeposited) } coinsDeposited -= item.price --item.count inventory[name] = item dispense(name) } }
Kuznetsov 重構了官方自動販賣機的代碼,去掉 guard
語句,並儘可能縮減了每一個函數的語句數量。恕我直言,我不喜歡這種重構,看完他的代碼來解釋下緣由。佈局
func vend(itemNamed name: String) throws { let item = try validatedItemNamed(name) reduceDepositedCoinsBy(item.price) removeFromInventory(item, name: name) dispense(name) } private func validatedItemNamed(name: String) throws -> Item { let item = try itemNamed(name) try validate(item) return item } private func reduceDepositedCoinsBy(price: Int) { coinsDeposited -= price } private func removeFromInventory(var item: Item, name: String) { --item.count inventory[name] = item } private func itemNamed(name: String) throws -> Item { if let item = inventory[name] { return item } else { throw VendingMachineError.InvalidSelection } } private func validate(item: Item) throws { try validateCount(item.count) try validatePrice(item.price) } private func validateCount(count: Int) throws { if count == 0 { throw VendingMachineError.OutOfStock } } private func validatePrice(price: Int) throws { if coinsDeposited < price { throw VendingMachineError.InsufficientFunds(coinsNeeded: price - coinsDeposited) } }
Kuznetsov 的主要目標是縮減函數的尺寸。但重構的結果倒是『將以前 18 行代碼驟增到 46 行』,而且將這些邏輯分散在至少八個函數中。這種形式的重構下降了代碼的可讀性,一個簡單的線性故事變成了一個混亂的集合,沒有清晰的業務邏輯。測試
重構以後,新的 vend
函數依賴七個方法調用。如今開始進入你的思惟殿堂,想象當用戶點擊了販賣按鈕,此刻你將注意力放在這些新觸發的方法調用上,爲了理解整個流程,不得不分散你的注意力在這些方法上反覆遊走。ui
Kuznetsov 將一個統一的函數分割開來,這裏我要引用一篇 George Miller 的論文:神奇數字 7。不只是由於 8 明顯比 1 大,更是由於『能集中注意力』纔是 Martin 簡化函數的主要目的。針對這些問題 Kuznetsov 的重構顯然是不及格的。翻譯
下面的批評有點不客氣,Kuznetsov 誤解了 guard
的做用。在他的文章中,guard
的做用是減小嵌套。我以爲他根本就不懂 guard
,正如我以前文章中的觀點,guard
一樣也是 assert/precondition
你們族中重要的一員:『通常意義上的 guard
語句定義了執行的先決條件,一樣也提供在不知足條件時,引導你們撤退的安全路線。』設計
Kuznetsov’s 從新設計的斷言被歸爲一個斷言樹。主功能函數 validateItemNamed
首先會調用 validate
,接着,validate
分別去調用其內部的兩個驗證方法: validateCount
和 validatePrice
。我認爲這種基於樹的佈局很難閱讀且不易維護,也增長了沒必要要的複雜性。
當錯誤發生時,你必需要從錯誤發生節點回溯到最初調用 try vend
地方。好比資金不足會致使 validatePrice
驗證失敗,而後退回到 validate
,再退回到 validatedItemNamed
,最後回到引起失敗的始做俑者 vend
。這只是一個簡單的錯誤,但卻走了很長一段路。咱們能夠認定:這種將『驗證任務』從『使用任務』中分離出來的作法是不正確的。
在蘋果的官方版本中,三條 guard
語句經過預先檢查『輸入』和『狀態』,來限制對核心功能的訪問。更重要的是,guard
說明了繼續執行下面代碼的先決條件。經過運用 guard
語句,Apple 在斷言(assertions)和動做(actions)之間創建了一種直接聯繫,即:若是測試經過,就執行這些動做。
斷言(assertions)和動做(actions)之間的協同定位相當重要。在未來作代碼審查時,能夠經過這些行爲(actions)的上下文來檢查這些測試,有必要的話,進行更新、修改、刪除這些操做也很方便。他們與被守護代碼之間,近似地創建起一條重要鏈接。
在代碼中我推薦使用 guard
來作基本的安全檢查,並堅持認爲蘋果官方(自動售貨機)纔是 guard
使用的正確姿式。最後總結一下:你或許有本身使用 guard
的方式,可是這樣作並不會對你的代碼帶來好處。
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。