深入理解Swift中单例模式的替换及Swift 3.0单例模式的实现

前言

除了 MVC、MVVM 之外,单例模式可以说是 iOS 开发中另一常见的设计模式。无论是 UIKit 或是一些流行的三方库,我们都能看到单例的身影。而我们开发者本身也会潜意识地将这些类库中的代码当作最佳实践并将其带入日常工作中,哪怕很多人都知道单例存在一些明显的缺陷。

针对单例的缺陷,本文将介绍一些替换或改造单例模式的方法来提升代码质量。

单例的优点

除了上面提到的模仿最佳实践之外,单例的流行肯定也有内在的原因和理由。例如:单例对象保证了只有一个实例的存在,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。 另一方面,全局单一对象也减少了不必要的对象创建和销毁动作提高了效率。

下面是一个典型的单例模式代码:

class UserManager {
 static let shared = UserManager()
 
 private init() {
 // 单例模式,防止出现多个实例
 }
 
 ....
}

extension UserManager {
 func logOut( ) {
 ...
 }
 
 func logIn( ) {
 ...
 }
}

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()
 }
}

单例的缺陷

虽然上面提到了单例的一些优点,但是这不能掩盖单例模式一些明显的缺陷:

  • 全局共享可修改的状态:单例模式的副作用之一就是那些共享状态量在 app 的生命周期内都可能发生修改,而这些修改可能造成一些位置错误。更为糟糕的是因为作用域和生命周期的特性,这些问题还非常难定位。
  • 依赖关系不明确:因为单例在全局都非常容易进行访问,这将是我们的代码变成所谓的 意大利面条 式的代码。单例与使用者的关系界限不明确,后期维护也非常麻烦。
  • 难以追踪测试:因为单例模式与 app 拥有同样的生命周期而生命周期内进行的任意修改,所以无法确保一个干净的实例用于测试。
  • 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了 “单一职责原则”。

依赖注入

与之间之间使用单例对象不同,这里我们可以在初始化是进行依赖注入。

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()
 }
}

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()
 }
 }
}

上面代码中的依赖关系明显比之前更为清晰,而且也更方便后期维护和编写测试实例。另外,通过 LogOutService 对象我们将某些特定服务抽离了出来,避免了单例中常见的臃肿状态。

协议化改造

将一个单例滥用的应用一次性全面改写为上面那样的依赖注入和服务化显然是一件非常耗时且不合理的事情。所以下面将会介绍通过协议对单例进行逐步改造的方法,这里主要的做法就是将上面 LogOutService 提供的服务改写为协议:

protocol LogOutService {
 func logOut()
}

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

protocol NavigationService {
 func showLoginScreen()
 func showProfile(for user: User)
 ...
}

定义好协议服务之后,我们让原有的单例遵循该协议。此时我们可以在不修改原有代码实现的同时将单例对象当作服务进行依赖注入。

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)
 }
}

Swift3.0 单例模式实现的几种方法-Dispatch_Once

在开发中需要使用单例模式是再寻常不过的了,正常我们的思路是使用GCD的dispatch_once这个API来写,然而在swift3.0中,苹果已经废弃了这个方法,不过不用担心,我们可以用别的方式来实现。

结合swift语言的特性,总结了以下几种写法:

  • 普通创建法
  • 静态创建法
  • struct创建法
  • 通过给DIspatchQueue添加扩展实现

注意:这里我希望大家除了使用还要会调用该对应的方法

1.普通创建法

//MARK - : 单例:方法1
 static let shareSingleOne = Single()

2.静态创建法

let single = Single()
class Single: NSObject {
 //-MARK: 单例:方法2
 class var sharedInstance2 : Single {
  return single
 }
}

3.struct创建法

 //-MARK: 单例:方法3
 static var shareInstance3:Single{
 struct MyStatic{
  static var instance :Single = Single()
 }
 return MyStatic.instance;
 }

4.通过给DispatchQueue添加扩展实现

public extension DispatchQueue { 
 
 private static var _onceTracker = [String]() 
 
 /** 
 Executes a block of code, associated with a unique token, only once. The code is thread safe and will 
 only execute the code once even in the presence of multithreaded calls. 
 
 - parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID 
 - parameter block: Block to execute once 
 */ 
 public class func once(token: String, block:()->Void) { 
 objc_sync_enter(self) 
 defer { objc_sync_exit(self) } 
 
 if _onceTracker.contains(token) { 
  return 
 } 
 
 _onceTracker.append(token) 
 block() 
 } 
} 

使用字符串token作为once的ID,执行once的时候加了一个锁,避免多线程下的token判断不准确的问题。

使用的时候可以传token

DispatchQueue.once(token: "com.vectorform.test") { 
 print( "Do This Once!" ) 
} 

或者使用UUID也可以:

private let _onceToken = NSUUID().uuidString 
 
DispatchQueue.once(token: _onceToken) { 
 print( "Do This Once!" ) 
} 

结语

单例模式并不是毫无可取之处,例如在日志服务、外设管理等场景下还是非常适用的。但是大多数时候单例模式由于依赖关系不明确以及全局共享可变状态可能会增加系统的复杂度造成一系列未知问题。如果你当前的代码中使用了大量的单例模式的话,我希望本文能够帮你从中解脱出来构建一个更健壮的系统。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对呐喊教程的支持。

声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:notice#nhooo.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。