更多内容,欢迎关注公众号:Swift花园
喜欢文章?不如来点赞关注吧
单例一直是被批评最多的设计模式。让我们来了解 iOS 项目中应当如何合理使用单例。
人人都在吐槽可怜的单例模式,甚至有人说它是反模式的,那么究竟什么是单例?为什么它这么糟糕?
什么是单例?
由于简单,单例非常流行,被广泛采用。一个单例类在整个应用生命周期内只允许存在一个实例。这个实例是通过一个静态属性访问,全局共享,就像一个全局变量一样。🌏
全局变量和状态
单例之所以名声不好是因为他们共享全局可变状态。全局这个词听起来令人有点骇人,哪怕对于经验丰富的开发者。全局状态和变量是副作用的温床。因为你可以从程序的任何地方访问全局变量,所以当你的类使用这些变量时,它们是有状态的,不安全的,紧耦合并且难以调试的。显而易见,在对象间以这种方式共享状态不是一种好的实践。🤮
副作用
你应该审视你的变量,将它们做隔离,尽可能减少代码的状态。这么做可以消除副作用,使你的代码更安全。看看下面的例子:
var global = 0
// 这里是别人写的代码
func square(_ x: Int) -> Int {
global = x
return x * x
}
global = 1;
var result = square(5)
result += global // 我们本以为global是1
print(result) //wtf 30,不应该是26吗?求平方函数是别人写的。因为某种原因,他想把输入存到你将会用到的同一个全局变量。当你调用这个函数时并没有意识到这一点,除非你去察看他的代码。想象一下,如果代码规模庞大,由多人构建,层层封装。。。额,BUG大军! 🐛🐛🐛
单例对象的神秘生活
单例一经创建便一直存续,它们的工作方式跟全局变量几乎一样。因此使用单例需要十分小心。你只能利用单例来管理那些会持续整个应用生命周期的状态。Swift 默认是非线程安全的,因此你在使用单例时,需要有面对多线程问题的心理准备。可是既然单例这么容易出问题,我们不是应该完全避免使用单例吗? 答案是不。 🚫
何时使用单例类?
举个例子,UIApplication 最有可能是单例,因为应用本来就只能有一个实例,并且它需要存续到应用被关闭。这是一个绝佳的单例范例。另外一个用例是 Logger 类。它也很适合用单例,因为开或者不开 logger 都不会影响到应用状态。没有别人会持有或者管理这个logger,只有你自己传递信息给它,因此状态不会被搞乱。 结论:控制台和 logger 类是很适合单例的场景。👏
Console.default.notice("Hello,我是一个单例!")实际上,在Apple的框架中有大量的单例场景,下面这份清单或许能给你一些灵感:
- HTTPCookieStorage.shared
- URLCredentialStorage.shared
- URLSessionConfiguration.default
- URLSession.shared
- FileManager.default
- Bundle.main
- UserDefaults.standard
- NotificationCenter.default
- UIScreen.main
- UIDevice.current
- UIApplication.shared
- MPMusicPlayerController.systemMusicPlayer
- GKLocalPlayer.localPlayer()
- SKPaymentQueue.default()
- WCSession.default
- CKContainer.default()
- 等等
单例很有用,但必须小心使用。
如果你想要把某样东西变成单例,先问自己几个问题:
- 有没有其他人可能持有,管理或者对这个东西负责?
- 是真的只有一个实例吗?
- 可以用全局状态变量代替吗?
- 是否存续在整个应用生命周期?
- 有没有其他选项?
Swift中如何创建单例?
Swift 中创建单例很容易。还是那句话,用之前三思,是否真的需要用?有没有更好的选择?
class Singleton {
static let shared = Singleton()
private init() {
// 不要忘记把构造器变成私有
}
}
let singleton = Singleton.shared其实,现在我们大家都经常在创建一个特定的单例对象,它叫做 App。通过它,我们可以应用中相关的各种全局状态属性聚合到同一个单例中。在典型的用法中,我们通过命名约定来帮助自己确定状态里装的是什么。💡
如何消除单例?
单例最常用的替代选项是依赖注入。首先,你应该将单例方法抽象为协议,然后用单例作为这个协议的默认实现。现在,你可以将单例或者重构的对象注入合适的地方。😎
typealias DataCompletionBlock = (Data?) -> Void
// 1. 抽象要求的函数
protocol Session {
func make(request: URLRequest, completionHandler: @escaping DataCompletionBlock)
}
// 2. 使你的单例遵循协议
extension URLSession: Session {
func make(request: URLRequest, completionHandler: @escaping DataCompletionBlock) {
let task = self.dataTask(with: request) { data, _, _ in
completionHandler(data)
}
task.resume()
}
}
class ApiService {
var session: Session
// 3. 注入单例对象
init(session: Session = URLSession.shared) {
self.session = session
}
func load(_ request: URLRequest, completionHandler: @escaping DataCompletionBlock) {
self.session.make(request: request, completionHandler: completionHandler)
}
}
// 4. 创建mock对象
class MockedSession: Session {
func make(request: URLRequest, completionHandler: @escaping DataCompletionBlock) {
completionHandler("Mocked data response".data(using: .utf8))
}
}
// 5. 编写测试
func test() {
let api = ApiService(session: MockedSession())
let request = URLRequest(url: URL(string: "https://localhost/")!)
api.load(request) { data in
print(String(data: data!, encoding: .utf8)!)
}
}
test()如你所见,实现单例真的很简单,但要决定单例的应用形式却不是件易事。我倒不认为单例是反模式的,显然它也不是,只是使用单例时要多加小心。😉
我的公众号
这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~
