關于 guard 的另一種觀點

作者:Erica Sadun,原文鏈接,原文日期:2016-01-01
譯者:walkingway;校對:saitjr;定稿:shanks

今天,iOS Dev 周刊 貼出一篇 Alexei Kuznetsov 的關于『從你的代碼中刪除 guard 』的文章。Kuznetsov 指出支持他這篇文章的理論依據主要來自于 Robert C. Martin,這位世界頂級軟件開發大師提出:代碼必須精簡。即關于函數存在兩條規則,第一條:函數應該保持精簡;第二條:沒有最精簡,只有更精簡。Alexei Kuznetsov 表示應將 Martin 的理論應用在今后的 Swift 開發中。

Kuznetsov 寫到『使用 guard 語句能有效減少函數中的嵌套數量,但 guard 存在一些問題。使用 guard 語句會使我們在一個函數中做更多的事情,以及維護多個級別的抽象。如果我們堅持短小、功能單一的函數,就會發現根本不需要 guard』。

我寫這篇文章的目的是為了反駁 Kuznetsov 提出的觀點,接下來我要說說我的看法。

代碼

下面的代碼片段來自于蘋果官方《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 函數依賴七個方法調用。現在開始進入你的思維殿堂,想象當用戶點擊了販賣按鈕,此刻你將注意力放在這些新觸發的方法調用上,為了理解整個流程,不得不分散你的注意力在這些方法上反復游走。

Kuznetsov 將一個統一的函數分割開來,這里我要引用一篇 George Miller 的論文:神奇數字 7。不僅是因為 8 明顯比 1 大,更是因為『能集中注意力』才是 Martin 簡化函數的主要目的。針對這些問題 Kuznetsov 的重構顯然是不及格的。

重構將『先決條件』視為一個單獨的任務

下面的批評有點不客氣,Kuznetsov 誤解了 guard 的作用。在他的文章中,guard 的作用是減少嵌套。我覺得他根本就不懂 guard,正如我之前文章中的觀點,guard 同樣也是 assert/precondition 大家族中重要的一員:『一般意義上的 guard 語句定義了執行的先決條件,同樣也提供在不滿足條件時,引導大家撤退的安全路線。』

Kuznetsov’s 重新設計的斷言被歸為一個斷言樹。主功能函數 validateItemNamed 首先會調用 validate,接著,validate 分別去調用其內部的兩個驗證方法: validateCountvalidatePrice。我認為這種基于樹的布局很難閱讀且不易維護,也增加了不必要的復雜性。

當錯誤發生時,你必須要從錯誤發生節點回溯到最初調用 try vend 地方。比如資金不足會導致 validatePrice 驗證失敗,然后退回到 validate,再退回到 validatedItemNamed,最后回到引發失敗的始作俑者 vend。這只是一個簡單的錯誤,但卻走了很長一段路。我們可以認定:這種將『驗證任務』從『使用任務』中分離出來的做法是不正確的。

在蘋果的官方版本中,三條 guard 語句通過預先檢查『輸入』和『狀態』,來限制對核心功能的訪問。更重要的是,guard 說明了繼續執行下面代碼的先決條件。通過?運用 guard 語句,Apple 在斷言(assertions)和動作(actions)之間建立了一種直接聯系,即:如果測試通過,就執行這些動作。

斷言(assertions)和動作(actions)之間的協同定位至關重要。在將來做代碼審查時,可以通過這些行為(actions)的上下文來檢查這些測試,有必要的話,進行更新、修改、刪除這些操作也很方便。他們與被守護代碼之間,近似地建立起一條重要連接。

在代碼中我推薦使用 guard 來做基本的安全檢查,并堅持認為蘋果官方(自動售貨機)才是 guard 使用的正確姿勢。最后總結一下:?你或許有自己使用 guard 的方式,但是這樣做并不會對你的代碼帶來好處。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,523評論 25 708
  • Hello Word 在屏幕上打印“Hello, world”,可以用一行代碼實現: 你不需要為了輸入輸出或者字符...
    restkuan閱讀 3,235評論 0 6
  • 領域驅動設計(DDD)旨在軟件設計過程中提煉領域模型,以領域模型為核心改善業務專家和軟件開發者的溝通方式,對企業級...
    MagicBowen閱讀 5,651評論 0 29
  • 帶著夏日的激情,我走進九月的巴黎。 留學?堂皇而美麗的借口!愛情的誘惑吧,這是他的城市。我想象的翅膀鼓滿了風,璀璨...
    繁花私語閱讀 355評論 0 3
  • 天際有幾片輪廓不太明顯的云,幾乎是與作為背景的明橘色與淺粉色糅雜在一起的天空融合在一塊。安德爾和苜蓿已經走到山林的...
    蘋香閱讀 221評論 0 0