美文网首页Swift开发iOS开发
关于 guard 的另一种观点

关于 guard 的另一种观点

作者: 梁杰_numbbbbb | 来源:发表于2016-01-14 22:30 被阅读80次

    作者: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

    相关文章

      网友评论

        本文标题:关于 guard 的另一种观点

        本文链接:https://www.haomeiwen.com/subject/mnkhkttx.html