Alexei Kuznetsov关于《从你的代码中删除guard)》一文在国外iOS开发者群中引起了许多讨论。Kuznetsov指出支持他这篇文章的理论依据主要来自于Robert C. Martin,这位世界顶级软件开发大师提出:代码必须精简。即关于函数存在两条规则,第一条:函数应该保持精简;第二条:没有最精简,只有更精简。Alexei Kuznetsov表示应将Martin的理论应用在今后的Swift开发中。
对此,Erica Sadun撰写了文章《关于guard的另一种观点》,来反驳Kuznetsov提出的观点。而本文作者DAVID OWENS II也同样给出了自己的想法。
Alexei Kuznetsov的《从你的代码中删除guard》一文让我不禁想起那些很多人信以为真的编程谣言。但在我看来,并不存在所谓的“标准编程方法”,必须具体问题具体分析后,再选择一条合适的路走下去。
在一些路的终点,风景总是美好的,尽管多多少少会有不完美,而且最终抵达的目的地不见得有最美的风景。对于开发者而言,编程环境很重要,而且要避免走一些弯路——通过上面的博文多少可以借鉴过来人的经验。
那么,哪些弯路(即编程禁忌)要尽力避开呢?
- 认为函数应为6-10行;
- 认为函数的“单一职责”就是做好一件事足矣。
第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
刚开始我并不清楚这是干嘛用的,后来仔细分析了其调用的代码,发现它主要是为了:
- 确保将条目(item)添加到字典里;
- 条目的数量不得为零;
- 存入的硬币数量不小于条目的价格。
不过它要求有4个函数和3层函数调用来实现上述目标。别忘了:4个函数的要求执行起来并非易事,很容易出错,当1个函数有了变动,其他函数也会受到影响。
举个例子:新函数addItem用于为自动贩卖机(vending machine)添加额外条目,但添加新条目会受到一定限制:
- 名称不能为空;
- 单词首字母必须大写(如Big Candy Bar);
- 价格必须低于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),软件工程师
翻译:张新慧
审校/责任编辑:唐小引