Swift 函式提前返回

極光推送發表於2018-09-18

簡評:函式提前返回主要的好處是:將每個錯誤處理進行分離,審查程式碼時不需要考慮多種複雜異常,我們可以吧注意力集中在也業務邏輯中,除錯程式碼時可以直接在異常中打斷點。

提前返回

首先來看一下需要改進的程式碼示例,我們構建一個筆記應用使用 NotificationCenter API,當筆記內容有變化時 Notification 來通知筆記列表變更,程式碼如下:

class NoteListViewController: UIViewController {
    @objc func handleChangeNotification(_ notification: Notification) {
        let noteInfo = notification.userInfo?["note"] as? [String : Any]

        if let id = noteInfo?["id"] as? Int {
            if let note = database.loadNote(withID: id) {
                notes[id] = note
                tableView.reloadData()
            }
        }
    }
}

上面的程式碼可以很好的工作,但是可讀性差了點。因為這段程式碼包含多重縮排和型別轉換。我們來嘗試改進這段程式碼。

  • 將方法提前返回,讓我們函式儘可能的快的返回。
  • 使用 guard 替代 if,以避免巢狀。
class NoteListViewController: UIViewController {
    @objc func handleChangeNotification(_ notification: Notification) {
        let noteInfo = notification.userInfo?["note"] as? [String : Any]

        guard let id = noteInfo?["id"] as? Int else {
            return
        }

        guard let note = database.loadNote(withID: id) else {
            return
        }

        notes[id] = note
        tableView.reloadData()
    }
}

將函式提前返回能夠將功能失敗的情況處理得更加清晰,這不僅提高了可讀性(更少的縮排,更少的巢狀),同時也有利於單元測試。

我們可以進一步改進程式碼,將獲取 noteID 和型別轉換的程式碼放在 Notification Extension 中,這樣就將 handleChangeNotification 業務邏輯和具體細節分離開來。修改後程式碼如下所示:

private extension Notification {
    var noteID: Int? {
        let info = userInfo?["note"] as? [String : Any]
        return info?["id"] as? Int
    }
}

class NoteListViewController: UIViewController {
    @objc func handleChangeNotification(_ notification: Notification) {
        guard let id = notification.noteID else {
            return
        }

        guard let note = database.loadNote(withID: id) else {
            return
        }

        notes[id] = note
        tableView.reloadData()
    }
}

這種結構還大大簡化了除錯的難度,我們可以直接在每個 guard 中 return 中新增斷點來截獲所有失敗情況,而不需要單步執行所有邏輯。

條件構造

當構造一個物件例項,非常普遍的需求是需要構建哪類物件取決於一系列的條件。

例如,啟動應用程式時顯示哪個 view controller 取決於:

  • 是否已經登入。
  • 使用者是否已經完成入職流程(onboarding flow)。

我們對這些條件的的實現可能是一系列的 if 和 else 語句,如下所示:

func showInitialViewController() {
    if loginManager.isUserLoggedIn {
        if tutorialManager.isOnboardingCompleted {
            navigationController.viewControllers = [HomeViewController()]
        } else {
            navigationController.viewControllers = [OnboardingViewController()]
        }
    } else {
        navigationController.viewControllers = [LoginViewController()]
    }
}

同樣的提前返回和 guard 語句可以提升程式碼可讀性,但是現在這種情況不是處理失敗情況,而是在不同條件下構建不同 view controller。

現在來改進這段程式碼,使用輕量級的工程模式,將構造初始介面移動到專門的函式中,該函式返回匹配條件的view controller。如下所示:

func makeInitialViewController() -> UIViewController {
    guard loginManager.isUserLoggedIn else {
        return LoginViewController()
    }

    guard tutorialManager.isOnboardingCompleted else {
        return OnboardingViewController()
    }

    return HomeViewController()
}


func showInitialViewController() {
    let viewController = makeInitialViewController()
    navigationController.viewControllers = [viewController]
}

由於 makeInitialViewController 方法是個純函式(不影響外部狀態,固定輸入能夠得到固定輸出),實際上影響外部狀態的只有一個地方 navigationController.viewControllers = [viewController] ,(在日常開發中狀態如果沒有得到很好的控制很容易引起 bug,所以使用更少狀態和減少對狀態的修改可以一定程度上減少 bug 出現的機率)。

條件控制

最後我們來看看,函式如何簡化複雜的條件邏輯。我們來構建一個 view controller 來顯示社交應用的評論功能,如果滿足三個條件則執行使用者對評論進行編輯。程式碼如下:

class CommentViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        if comment.authorID == user.id {
            if comment.replies.isEmpty {
                if !comment.edited {
                    let editButton = UIButton()
                    ...
                    view.addSubview(editButton)
                }
            }
        }

        ...
    }
}

這裡使用了 3 個 if 巢狀邏輯,每次重新審查程式碼都會比較困擾,更具之前的經驗我們可以對程式碼進行優化,新增 Comment extension:

extension Comment {
    func canBeEdited(by user: User) -> Bool {
        guard authorID == user.id else {
            return false
        }

        guard comment.replies.isEmpty else {
            return false
        }

        return !edited
    }
}

class CommentViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        if comment.canBeEdited(by: user) {
            let editButton = UIButton()
            ...
            view.addSubview(editButton)
        }

        ...
    }
}

原文:Early returning functions in Swift

相關文章