返回 登录
3

关于Swift中是否应该弃用guard的思考

Alexei Kuznetsov关于《从你的代码中删除guard)》一文在国外iOS开发者群中引起了许多讨论。Kuznetsov指出支持他这篇文章的理论依据主要来自于Robert C. Martin,这位世界顶级软件开发大师提出:代码必须精简。即关于函数存在两条规则,第一条:函数应该保持精简;第二条:没有最精简,只有更精简。Alexei Kuznetsov表示应将Martin的理论应用在今后的Swift开发中。

对此,Erica Sadun撰写了文章《关于guard的另一种观点》,来反驳Kuznetsov提出的观点。而本文作者DAVID OWENS II也同样给出了自己的想法。

Alexei Kuznetsov的《从你的代码中删除guard》一文让我不禁想起那些很多人信以为真的编程谣言。但在我看来,并不存在所谓的“标准编程方法”,必须具体问题具体分析后,再选择一条合适的路走下去。

在一些路的终点,风景总是美好的,尽管多多少少会有不完美,而且最终抵达的目的地不见得有最美的风景。对于开发者而言,编程环境很重要,而且要避免走一些弯路——通过上面的博文多少可以借鉴过来人的经验。

那么,哪些弯路(即编程禁忌)要尽力避开呢?

  1. 认为函数应为6-10行;
  2. 认为函数的“单一职责”就是做好一件事足矣。

第1条随意定义了代码的质量和复杂度,并不保证能解决问题;还妄下定论称代码越短越好——而事实是,代码越短就越复杂。而我觉得代码干净利落比长短要重要得多。

以下是Robert C. Martin对于代码长度的看法:

The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.
函数的第一原则——小;第二原则——更小。

下面这段话总结得更好:

Functions should be as small as possible to do there job, but no smaller than that.
函数应做到小,但刚刚好才是最好。

很多人误以为“单一职责”就是搞定一个action那么简单,忍不住过早进行代码重构——一会儿我会举一些博文中的例子。

一个函数执行一个任务时,通常要经过多个步骤——鉴于我们需要vend()函数来避免复制逻辑,这一点应该很好理解。

过早重构

现在来看看这篇博文中的内容。以下是来自Apple示例的Swift代码:

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)
    }
}

这是从博文中摘出的重构版本:

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)
    }
}

分解后进行分析:

vend(itemNamed name: String) throws

博文作者认为重构版本更好。但要注意:首先,函数职责从头到尾都是一样的,所以使用API也不会引起任何改变。了解这一点至关重要,因为重构就是为了拆分不属于一类的功能。

private func validatedItemNamed(name: String) throws -> Item

刚开始我并不清楚这是干嘛用的,后来仔细分析了其调用的代码,发现它主要是为了:

  1. 确保将条目(item)添加到字典里;
  2. 条目的数量不得为零;
  3. 存入的硬币数量不小于条目的价格。

不过它要求有4个函数和3层函数调用来实现上述目标。别忘了:4个函数的要求执行起来并非易事,很容易出错,当1个函数有了变动,其他函数也会受到影响。

举个例子:新函数addItem()用于为自动贩卖机(vending machine)添加额外条目,但添加新条目会受到一定限制:

  1. 名称不能为空;
  2. 单词首字母必须大写(如Big Candy Bar);
  3. 价格必须低于100。

我很确定,可以在这儿更新validateItem()函数以添加这些新的要求。我们不仅要确认什么情况下调用vent(),还要清楚vent()对于自动贩卖机中不满足要求的数据是没有用的。

下面的函数可不是摆设哦。这种特定类型的重构决定了我得在真正编码的时候解决这一类问题。

reduceDepositedCoinsBy(price: Int)

假设已调用了validate(),这个函数会导致数据损坏。在使用之前,必须确保此操作是合法的,否则就没有意义了。

removeFromInventory(var item: Item, name: String)

这个函数同样要注意数据损坏的问题!

itemNamed(name: String) throws -> Item

这个函数有点儿意思——如果Swift有抛出异常的话,它就没必要存在了。不过,原则上来讲,不是说这个函数不好,而是它太容易出错,是典型的guard语句。

private func itemNamed(name: String) throws -> Item {
    guard let item = inventory[name] {
        throw VendingMachineError.InvalidSelection
    }
    return item
}

这个客观上来说更好一些,能确保guard语句后唯一存在的代码路径是容许字典里有那个条目的;同时还保证,如果字典里没有那个条目的话,能尽早查出来。

总结

千万不能一时兴起,随随便便就进行代码重构,否则很容易将代码复杂化,导致代码中出现错误路径。

我的指导原则是:一个函数应该专注于自己的职责,做好本分就够了,经过多少步骤都只是华而不实的考虑而已。

英文来源:RE: WHY SWIFT GUARD SHOULD BE AVOIDED
作者:David Owens II(@owensd),软件工程师
翻译:张新慧
审校/责任编辑:唐小引

本文为CSDN编译整理,未经允许不得转载,如需转载请联系mobile#csdn.net(#换成@)

评论