什么是单例?如何使用Swift创建单例?

783 阅读5分钟

单例模式是一个在软件开发中常见的设计模式。尽管它很流行,但是通常被认为是反设计模式的。为什么这么说呢?请看下文中Swift中如何创建单例模式。

什么是单例

单例这个名词理解起来很简单。单例模式保证一个类只实例化一个实例。 如果你使用过Apple的框架,那么你已经接触过单例模式了。比如:

// 共享 URL Session
let sharedURLSession = URLSession.shared

// 默认 File Manager
let defaultFileManager = FileManager.default

// 标准 User Defaults
let standardUserDefaults = UserDefaults.standard

// 默认 Payment Queue
let defaultPaymentQueue = SKPaymentQueue.default()

单例模式是十分有用的模式。有时候你想确保一个类只是实例化一个实例,整个程序只使用这个实例。首先考虑的并且唯一能达到这个目标的就是单例模式。
StoreKit框架中的默认支付队列就是一个很好的例子。你的应用程序永远不应该创建一个SKPaymentQueue类的实例。操作系统会使用StoreKit框架创建一个你的应用程序可用的支付队列。可以通过SKPaymentQueue类的类方法default()访问这个默认的支付队列。

全局访问(Global Access)

全局访问是使用单例模式的副产品,但通常是采用单例模式的真正原因。
不幸的是,很多开发者在他们程序的任意地方都可以通过单例模式轻易访问单例对象。比如默认支付队列可以通过default() 类方法来访问。这就意味着项目的任何地方都可以访问默认支付队列。虽然这很便利,但是这种便利有一定的代价。
如果想了解围绕单例模式的更多问题,推荐阅读此文章Are Singletons Bad

如何使用Swift创建单例

本小结介绍了两种Swift中使用单例模式的方法。尽管第一种方法不应该被使用。

全局变量(Global Variables)

创建单例最直接的方式是定义一个全局变量。

let sharedNetworkManager = NetworkManager(baseURL: API.baseURL)

class NetworkManager {

    // MARK: - Properties

    let baseURL: URL

    // Initialization

    init(baseURL: URL) {
        self.baseURL = baseURL
    }

}

通过在项目的全局命名空间中定义一个变量,任何在该模块中的对象都可以访问这个实例对象。
在Swift中,全局变量是惰性初始化的。这就意味着当第一次真正引用这个全局变量时,该变量才会被初始化。 Swift中一个更为便利的初始化方式是使用dispatch_once函数。该函数保证了初始化器只被执行一次。如果你只想初始化单例对象一次,这个方式就非常重要。
使用全局变量有一些弊端。最重要的问题就是让全局命名空间 变得混乱了。另一个问题是NetworkManager类的初始化器不能被声明为私有的(private)。这就意味着该类可以实例化众多实例。下面展示一种更为好的单例模式使用方式。

静态变量和私有初始化器

几年以前,Swift引入了静态变量和访问控制,这给我们在Swift使用单例模式提供了新的途径。比使用全局变量更加清晰和优雅。

class NetworkManager {

  // MARK: - Properties

  static let shared = NetworkManager(baseURL: API.baseURL)

  // MARK: -

  let baseURL: URL

  // Initialization

  private init(baseURL: URL) {
      self.baseURL = baseURL
  }

}
//访问NetworkManager实例
func someFunction(){
    let manager = NetworkManager.shared
    print(manager)
}

和第一个全局变量的使用方式相比,本方式的几个使用细节改变了。首先,初始化器变为私有的。这意味着只有NetworkManager类可以创建自身的实例。这是一个重要的优势。其次,我们将shared变量声明为静态常量属性。这个变量使其他对象可以访问NetworkManager单例对象。
使用lazy关键字标记静态变量并不是必要的。如前文所说,全局变量和静态变量的初始化默认就是惰性初始化(lazy init)。这是使用本方法设计单例的另外一个优势。
下面分享几个稍显复杂的创建单例的方式。和上面的主要区别就是使用闭包(closure)来初始化单例对象。这允许你对单例对象进行更复杂的初始化和配置。

class NetworkManager {

  // MARK: - Properties

  private static var sharedNetworkManager: NetworkManager = {
      let networkManager = NetworkManager(baseURL: API.baseURL)

      // Configuration
      // ...

      return networkManager
  }()

  // MARK: -

  let baseURL: URL

  // Initialization

  private init(baseURL: URL) {
      self.baseURL = baseURL
  }

  // MARK: - Accessors

  class func shared() -> NetworkManager {
      return sharedNetworkManager
  }

}
//访问NetworkManager实例
func someFunction(){
    let manager = NetworkManager.shared
    print(manager)
}

静态变量被声明未私有的,单例对象只能通过shared()类方法来访问。

Cocoa 和单例

记住上述的几种单例模式使用方式,我们可以模仿很多Cocoa框架在Swift中的使用接口:

// Shared URL Session
let sharedURLSession = URLSession.shared

// Default File Manager
let defaultFileManager = FileManager.default

// Standard User Defaults
let standardUserDefaults = UserDefaults.standard

// Default Payment Queue
let defaultPaymentQueue = SKPaymentQueue.default()

单例模式不好吗?

Are Singletons Bad 中,作者介绍了一个项目使用单例模式的几种问题。建议要谨慎的使用单例。如果你要创建一个单例,冷静一下,思考是否有其他方式可能忽略了?是否必须要使用单例模式?
尽管单例并没有本质上的错误,但是很多开发者会因为错误的原因使用它:便利。他们将全局变量伪装成单例。

依赖注入

尽管你已经决定在项目中使用单例,这不意味着你应该让项目的任何地方都可以访问单例。你仍然可以通过依赖注入把单例传输给任何需要它的对象。
通过采用依赖注入来传递单例,让类接口保持清晰透明。换句话说,类的接口描述了它的依赖关系。这是非常非常有用的。它表明了该类需要哪些对象来执行其职责。

原文链接:cocoacasts.com/what-is-a-s…