HR's Blog

Swimming 🏊 in the sea🌊of code!

0%

Singleton

.

该文讲述了三种方法来创建单例, 各种方法的优点以及缺点,该文也介绍了另外一篇文章单例的Are Singletons Bad。

原文地址

What Is a Singleton and How To Create One In Swift

The singleton pattern is a widely used design pattern in software development. Despite its popularity, it’s often considered an anti-pattern. Why is that? In this episode, I explain what the singleton pattern entails and how to create singletons in Swift.

What Is a Singleton

Singletons are easy to understand. The singleton pattern guarantees that only one instance of a class is instantiated. That’s simple. Right?

The singleton pattern guarantees that only one instance of a class is instantiated.
单例模式保证只实例化一个类的一个实例。

If you’ve worked with Apple’s frameworks, then chances are that you’ve already used the singleton pattern. Take a look at these examples. They probably look familiar.

1
2
3
4
5
6
7
8
9
10
11
// 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()

The singleton pattern is a very useful pattern. There are times that you want to make sure only one instance of a class is instantiated and that your application only uses that instance. That’s the primary and only goal of the singleton pattern.

The default payment queue of the StoreKit framework is a fine example. An application should never create an instance of the SKPaymentQueue class. The operating system uses the StoreKit framework to create a payment queue, which your application can use. The default payment queue is accessible through the default() class method of the SKPaymentQueue class. This is a good example of how the singleton pattern should be applied.

Global Access

But the singleton pattern has a side effect that’s often the true reason for adopting the singleton pattern, global access. But having global access to the singleton object is no more than a side effect of the singleton pattern.

Unfortunately, many developers use the singleton pattern to have easy access to the singleton object from anywhere in their project. The default payment queue is accessible through the default() class method. This means that any object in a project can access the default payment queue. While this is convenient, that convenience comes at a price.

If you want to learn more about the problems surrounding the singleton pattern, then I recommend reading Are Singletons Bad. In that article, I discuss this topic in more detail.

How to Create a Singleton In Swift

In this episode, I list two recipes for implementing the singleton pattern in Swift. The first implementation shouldn’t be used, though. It merely illustrates a few concepts of the Swift language. 两种实现的方法,但是不应该用第一个来实现,仅仅是用他来说明swift语言的一些概念。

Global Variables

The most straightforward technique to create a singleton is by defining a global variable.最简单最直接的办法就是创建一个全局变量。

1
2
3
4
5
6
7
8
9
10
let sharedNetworkManager = NetworkManager(baseURL: API.baseURL)

class NetworkManager {
// MARK: - Properties
let baseURL: URL
// Initialization
init(baseURL: URL) {
self.baseURL = baseURL
}
}

定义在全局的变量,任何在module里的对象都可以访问该变量。
By defining a variable in the global namespace of the project, any object in the module has access to the singleton object. We could, for example, access the singleton object in the application(_:didFinishLaunchingWithOptions:) method of the AppDelegate class.

1
2
3
4
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
print(sharedNetworkManager)
return true
}

In Swift, global variables are initialized lazily. This means that the initializer is run the first time the global variable is referenced. Swift中全局变量只有被引用的时候才会被创建。

An added benefit of Swift’s approach is that the initialization is performed using the dispatch_once function. It guarantees that the initializer is invoked only once. That’s important since you only want to initialize the singleton object once.

Using a global variable has several shortcomings. The most important problem is cluttering the global namespace. Another downside is that the initializer of the NetworkManager class cannot be declared private. This means that multiple instances of the class can be instantiated. Let me show you a much better, and my preferred, implementation in Swift.
使用全局变量来申明单例有以下的缺点,第一就是污染了全局的命名空间。其二就是单例的初始化方法没有办法声明为private,这也就没有办法保证单例的唯一性。

Static Property and Private Initializer

A few years ago, Swift introduced static properties and access control to the language. This opened up an alternative approach to implementing the singleton pattern in Swift. It’s much cleaner and elegant than using a global variable. Take a look at the updated example.

1
2
3
4
5
6
7
8
9
10
11
class NetworkManager {
// MARK: - Properties
static let shared = NetworkManager(baseURL: API.baseURL)
// MARK: -
let baseURL: URL
// Initialization
private init(baseURL: URL) {
self.baseURL = baseURL
}
}

Accessing the singleton is intuitive and it clearly conveys that we’re dealing with a singleton.

1
2
3
4
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
print(NetworkManager.shared)
return true
}

这里有个几个实现细节的变化,第一就是初始化方法是私有的。也就意味着只有NetworkManager类能创建自己的实例。这是至关重要的第一点
Several implementation details have changed. First, the initializer is private. It means that only the NetworkManager class can create instances of itself. That’s a significant advantage.

第二我们申明了sharedstatic constant property属性,这个属性允许其他的对象访问到这个单例。
Second, we declared the shared static constant property. This property gives other objects access to the singleton object of the NetworkManager class.

这里不需要给static properties的属性加上lazy关键字,之前提到过全局变量默认的就是lazy的。
It isn’t necessary to mark static properties with the lazy keyword. Remember what I said earlier, the initializer of global variables and static properties are executed lazily by default. That’s another benefit.

I want to share one more implementation of the singleton pattern. This implementation is a bit more complex. The main difference is that the singleton object is instantiated in a closure, allowing for a more complex initialization and configuration of the singleton object.这里我将分享另外一种单例的实现,这种实现稍微有点复杂,主要的不同就是,对象的初始化是在一个closure中完成的,这也就允许更复杂的初始化和单例自身的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
}
}

The static property is declared private. The singleton object is accessible through the shared() class method.

1
2
3
4
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
print(NetworkManager.shared())
return true
}

Cocoa and Singletons

With these implementations in mind, we have mimicked the interface many Cocoa frameworks have adopted in Swift.

1
2
3
4
5
6
7
8
9
10
11
// 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

In Are Singletons Bad, I explain in detail what type of problems the singleton pattern can introduce in a project. My advice is to use singletons sparingly. Very sparingly. If you’re about to create a singleton, take a moment and step back. Is there another option you may be overlooking? Is it absolutely necessary to use the singleton pattern?

Even though there’s nothing inherently wrong with singletons, most developers use it for the wrong reasons, that is, convenience. They disguise global variables as singletons.

Dependency Injection

Even if you decide to use singletons in a project, that doesn’t mean you have to access them from anywhere in your project. You can still use dependency injection to pass the singleton object to the objects that need it.

By adopting dependency injection to pass singletons around, the interface of your class remains clear and transparent. In other words, the interface of the class describes its dependencies. This is very, very useful. It immediately shows which objects the class needs to perform its duties.

One of the things I miss about Objective-C is the header file. The header file summarizes the public interface of a class, including the dependencies of the class. This is sometimes less obvious in Swift.