為什么Swift中應該避免使用guard語句
自從guard語句在Swift中出現(xiàn)以來,就引起了大量的討論。講真,guard確實簡化了代碼,且提高了代碼的可讀性,但它就是靈丹妙藥了嗎?
小函數(shù)
關于函數(shù)的大小,人們也討論了很多。很明顯,函數(shù)體應該是短小的,而且是越短越好。沒有人想去讀、理解或者重構大函數(shù)。但是,函數(shù)應該多大才是正確的呢?
函數(shù)的首要準則是--短。次要準則是--更短。 ---羅伯特`C`馬丁
更具體點說,馬丁認為函數(shù)的長度應該小于六行,絕對不能大于十行。
規(guī)則雖簡,療效顯著,你可以看到代碼立刻就變得易懂多了。以前,你需要記住一個有著30行代碼,若干縮進層次,若干中間變量的函數(shù)。而現(xiàn)在只需要記住十個名字一目了然的函數(shù)。
單一職責
單一職責這件事也被說了很久。這條規(guī)則不僅適用于對象,也同樣適用于函數(shù)。很顯然,每一個函數(shù)應該只做一件事情,但是人們一次兩次地違反此規(guī)則,似乎大部分是因為函數(shù)的大小。如果將一個30行的函數(shù)重構為十個3行的函數(shù),那么在函數(shù)層次自然而然地遵循單一職責規(guī)則了。
單層抽象
人們對函數(shù)的單層抽象討論得并不多.它是一個有助于寫出單一職責函數(shù)的工具。
什么是單層抽象? 簡單地說,就是高抽象層次的代碼,例如控制進程的代碼中不應該混雜著小的細節(jié),例如變量自增或者布爾值檢驗。舉個栗子。
下面例子出自The Swift Programming Language book。
- 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)
- }
- }
當然,vend(itemNamed:)只是使用gaurd語句的一個例子,不過你經(jīng)常能在生產(chǎn)代碼中看到相似的函數(shù)。這個函數(shù)就出現(xiàn)了三個上面說到的問題:
- 它相當?shù)拈L,有十六行,很多部分由空行隔開。
- 它做了好幾件事:通過名稱拿到一件商品,驗證參數(shù)合法性,然后是出售商品的邏輯。
- 它有好幾個抽象層次。最高層的出售過程被隱藏在了更細層次的細節(jié)當中了,例如布爾值驗證,特殊常量的使用,數(shù)學運算等等。
這個函數(shù)重構后是什么樣呢?
- 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)
- }
- }
雖然總行數(shù)變多了,但你應該記住,代碼行數(shù)并不是它的最終目的。
重構后的代碼相對于舊版的多了幾個優(yōu)點:
- 核心的出售函數(shù)變小了,而且只包含了出售一個商品的步驟的高層邏輯.如果讀者對細節(jié)不感興趣,通過快速地看這個高層函數(shù),她就明白了售賣過程。
- 這些函數(shù)更好地遵守了單一職責原則.其中有一些還可以進一步分解地更小,不過即使是當前的形式,它們都更易讀易懂.它們將老的一大串的代碼分解成了更小的,一目了然的代碼塊。
- 每個函數(shù)只負責單一層次邏輯的抽象。讀者可以根據(jù)需要在不同層次間移動。那出售的過程是什么樣的呢?根據(jù)名稱確定商品是否有效,然后減少顧客的余額,再將商品從存貨清單中移除,最后顯示此商品已賣出?怎么知道商品是否有效呢?通過檢查數(shù)量和價格,那怎么知道確切的數(shù)量呢?通過和0做比較。如果讀者對細節(jié)毫無興趣,他完全可以忽略這部分內(nèi)容。
結論
Guard語句很便于用來減少結構體和函數(shù)的嵌套,但是問題不是guard本身,而是它的使用。Guard語句會促使你寫出能做好幾件事、有多層抽象層次的大函數(shù)。只要保證所寫的函數(shù)小而明確,你根本無需guard語句。
相關閱讀