咱們彷佛之前已經達成了共識,「單例模式很好用,但不能濫用」。可是在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
譯者補充:測試
- 僅僅使用一次的模塊,能夠不使用單例,由於它會一直佔用內存,能夠採用在對應的週期內維護成員實例變量進行替換。
- 單例保存用戶數據,退出登錄時須要清楚,這時若是有異步存儲任務就可能產生髒數據。
在以前的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
核心對象之間建立更清晰關係的一種好方法。
做爲示例,讓咱們仔細看看如何實現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
參考: