iOS 避免單例濫用

咱們彷佛之前已經達成了共識,「單例模式很好用,但不能濫用」。可是在Apple和第三方Swift框架中開發人員還在大量的使用它。git

"單例就是披着羊皮的全局狀態" -- Miško Heverygithub

今天咱們看一下單例使用的確切問題,並探索如何避免濫用。swift

爲何單例如此受歡迎

單例模式爲何在iOS開發中這麼流行呢?若是大多數開發人員都贊成應避免濫用它們,爲何仍在被大量使用?api

我認爲有兩個緣由。首先最主要緣由是Apple內部都在常用它,你們就會把蘋果的作法當成「最佳實現」模仿、流傳。安全

第二個緣由是它確實提供了便利。單例一般爲咱們訪問核心的值和對象提供了捷徑,由於它們基本上能夠從任何地方訪問。看下面這個例子,咱們想在ProfileViewController中顯示當前登陸的用戶名,並在點擊按鈕時註銷用戶:架構

class ProfileViewController: UIViewController {
    private lazy var nameLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.text = UserManager.shared.currentUser?.name
    }

    private func handleLogOutButtonTap() {
        UserManager.shared.logOut()
    }
}
複製代碼

像上面這樣把用戶信息和帳戶處理功能封裝在UserManager單例中,確實很是方便(並且很常見!)。 那麼使用這種模式到底有什麼很差呢? 🤔框架

單例模式有什麼很差?

在討論諸如模式和架構之類的問題時,咱們很容易陷入過於理論化的陷阱。使代碼在理論上是「正確的」並遵循全部最佳實踐和原則,這是很好的作法。但現實每每很殘酷,咱們須要視狀況而定。異步

那麼單例模式一般會引發什麼具體問題,爲何要避免這些問題呢?我傾向於避免使用單例的三個主要緣由是:ide

  • 它們是全局可變的共享狀態。它們的狀態會自動在整個應用程序中共享,而且當狀態被意外更改時,一般會發生錯誤。易錯。
  • 單例和依賴於它們的代碼之間的關係一般沒有很好地定義。因爲單例很是方便且易於訪問,普遍使用它們一般會致使很難維護,像「意大利麪條式代碼」,而對象之間沒有明確的分隔。隱性依賴。
  • 管理其生命週期可能很棘手。因爲單例在應用程序的整個生命週期中都處於活動狀態,所以對其進行管理可能很是困難,而且它們一般依靠可能爲空的(optional)的值。這也使得依賴單例的代碼真的很難測試,由於在每一個測試用例中都不能輕易從「初始狀態」開始。不便於測試。

譯者補充:測試

  1. 僅僅使用一次的模塊,能夠不使用單例,由於它會一直佔用內存,能夠採用在對應的週期內維護成員實例變量進行替換
  2. 單例保存用戶數據,退出登錄時須要清楚,這時若是有異步存儲任務就可能產生髒數據。

在以前的ProfileViewController示例中,咱們已經能夠看到這3個問題的跡象。咱們不能清除的判斷頁面是依賴UserManager的,並且單例包含一個可能爲空的對象currentUser,編譯時並不能檢測出來,即在頁面展現是可能currentUser爲nil。看起來就是一個等待觸發的Bug😬。

依賴注入

與其讓ProfileViewController經過單例訪問依賴數據,不如將它們注入其init方法中。 在這裏,咱們將用戶信息以及可用於執行註銷操做的LogOutService做爲必傳參數:

class ProfileViewController: UIViewController {
    private let user: User
    private let logOutService: LogOutService
    private lazy var nameLabel = UILabel()

    init(user: User, logOutService: LogOutService) {
        self.user = user
        self.logOutService = logOutService
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.text = user.name
    }

    private func handleLogOutButtonTap() {
        logOutService.logOut()
    }
}
複製代碼

結果更加清晰,更易於管理。 如今,咱們的代碼安全地依賴始終存在的Model,而且具備清晰的API執行註銷操做。 一般,將各類單例和管理器(managers)重構爲清晰、分開的Services是在App核心對象之間建立更清晰關係的一種好方法。

Services

做爲示例,讓咱們仔細看看如何實現LogOutService。 它也將「依賴項」用於其基礎服務,並提供了一個漂亮且定義明確的API,僅用於作一件事【註銷】。

class LogOutService {
    private let user: User
    private let networkService: NetworkService
    private let navigationService: NavigationService

    init(user: User,
         networkService: NetworkService,
         navigationService: NavigationService) {
        self.user = user
        self.networkService = networkService
        self.navigationService = navigationService
    }

    func logOut() {
        networkService.request(.logout(user)) { [weak self] in
            self?.navigationService.showLoginScreen()
        }
    }
}
複製代碼

重構

從大量使用單例改爲充分利用Services的代碼,依賴項注入和本地狀態可能很是困難且耗時。 花費時間不說,有時可能甚至須要巨大的重構。

不用擔憂,「使用協議來移除單例」的方式能夠助你一臂之力。

不須要一次重構全部單例並建立新的Service類,咱們能夠簡單地將Service定義爲協議,以下所示:

protocol LogOutService {
    func logOut()
}

protocol NetworkService {
    func request(_ endpoint: Endpoint, completionHandler: @escaping () -> Void)
}

protocol NavigationService {
    func showLoginScreen()
    func showProfile(for user: User)
    ...
}
複製代碼

而後,咱們可讓單例遵循咱們的新服務協議,從而輕鬆地將單例做爲「服務」進行改造。 在許多狀況下,咱們甚至不須要進行任何實現上的修改,只需將單例做爲服務傳遞便可。

一樣的技術也能夠用於改造咱們App中的其餘核心對象,而這些核心對象可能一直以「單例模式」的方式使用,例如使用AppDelegate進行頁面導航。

extension UserManager: LoginService, LogOutService {}

extension AppDelegate: NavigationService {
    func showLoginScreen() {
        navigationController.viewControllers = [
            LoginViewController(
                loginService: UserManager.shared,
                navigationService: self
            )
        ]
    }

    func showProfile(for user: User) {
        let viewController = ProfileViewController(
            user: user,
            logOutService: UserManager.shared
        )

        navigationController.pushViewController(viewController, animated: true)
    }
}
複製代碼

如今,咱們能夠經過使用「依賴項注入」和Services開始把全部的view controller中的單例去掉了。無需進行大量的重構和修改🎉!

總結

單例並非無可救藥,可是在許多狀況下,單例確實帶來了一系列問題。咱們能夠經過在對象之間建立更明肯定義的關係並使用依賴注入來避免這些問題。

做者 johnsundell

翻譯整理:樂Coding

參考:

satanwoo.github.io/2016/04/11/…

objccn.io/issue-13-2/

www.swiftbysundell.com/articles/av…

相關文章
相關標籤/搜索