一、单例模式是什么鬼?
单例模式可能是设计模式家族里最出名的一个了,简单来说它就是保证一个类只有一个实例,而且提供一个全局访问点。想象一下,你家里只有一个电饭煲,全家人要用的时候都去同一个地方拿,这就是单例的精髓。
在Swift中实现单例特别简单,Swift 1.2之后有了更优雅的实现方式。先看个最简单的例子:
class MySingleton {
// 1. 静态常量保存唯一实例
static let shared = MySingleton()
// 2. 私有化初始化方法
private init() {}
func doSomething() {
print("单例在工作...")
}
}
// 使用示例
MySingleton.shared.doSomething()
这个实现有几个关键点:
- 使用static let创建类级别的常量
- 将init方法设为private,防止外部创建实例
- 通过shared这个静态属性访问唯一实例
二、为什么我们需要单例?
单例模式在以下场景特别有用:
- 全局配置管理:比如App的主题设置、用户偏好等
- 资源共享:数据库连接池、网络请求管理器
- 需要频繁访问的对象:日志记录器、分析工具
举个实际的例子,我们来看一个网络请求管理器的实现:
class NetworkManager {
static let shared = NetworkManager()
private let session: URLSession
private init() {
// 配置URLSession
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
self.session = URLSession(configuration: config)
}
func request(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
let task = session.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error))
} else if let data = data {
completion(.success(data))
}
}
task.resume()
}
}
// 使用示例
let url = URL(string: "https://api.example.com/data")!
NetworkManager.shared.request(url: url) { result in
switch result {
case .success(let data):
print("收到数据: \(data.count)字节")
case .failure(let error):
print("请求失败: \(error)")
}
}
这个网络管理器有几个优点:
- 整个App共享同一个URLSession实例
- 统一管理网络配置
- 避免重复创建URLSession带来的开销
三、单例模式的黑暗面
单例虽然好用,但滥用会导致很多问题:
- 全局状态难以追踪:修改单例的状态可能影响整个App
- 测试困难:单例很难被mock或替换
- 内存泄漏风险:单例生命周期与App相同,可能持有不需要的资源
来看个反面教材:
class UserManager {
static let shared = UserManager()
var currentUser: User?
var authToken: String?
var lastLoginDate: Date?
var favorites: [Int] = []
// ...越来越多的全局状态
private init() {}
}
// 到处都在访问和修改这些状态
UserManager.shared.currentUser = someUser
UserManager.shared.authToken = token
// ...
这种"上帝对象"式的单例会带来很多问题:
- 状态分散在各处修改,难以追踪
- 耦合度过高
- 难以进行单元测试
四、如何正确使用单例
为了避免滥用单例,我们可以遵循以下原则:
- 严格控制单例职责:一个单例应该只负责一件事
- 考虑依赖注入:在某些场景下,依赖注入可能是更好的选择
- 提供重置方法:便于测试和状态清理
改进后的用户认证管理示例:
protocol AuthServiceProtocol {
func login(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void)
func logout()
}
class AuthService: AuthServiceProtocol {
static let shared = AuthService()
private var authToken: String?
private init() {}
func login(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void) {
// 模拟网络请求
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
let user = User(id: UUID(), email: email)
self.authToken = "模拟token"
completion(.success(user))
}
}
func logout() {
authToken = nil
}
// 测试辅助方法
func resetForTesting() {
authToken = nil
}
}
// 使用依赖注入的ViewController
class LoginViewController: UIViewController {
let authService: AuthServiceProtocol
init(authService: AuthServiceProtocol = AuthService.shared) {
self.authService = authService
super.init(nibName: nil, bundle: nil)
}
// ...其他实现
}
这个改进版有这些优点:
- 遵循单一职责原则
- 通过协议支持测试替身
- 支持依赖注入,提高可测试性
五、单例与线程安全
单例的初始化在Swift中是线程安全的,但单例内部的属性访问可能需要额外保护。来看个线程安全的缓存实现:
class CacheManager {
static let shared = CacheManager()
private var storage: [String: Any] = [:]
private let queue = DispatchQueue(label: "com.example.cache", attributes: .concurrent)
private init() {}
func set(_ value: Any, forKey key: String) {
queue.async(flags: .barrier) {
self.storage[key] = value
}
}
func get(forKey key: String) -> Any? {
queue.sync {
return self.storage[key]
}
}
}
// 使用示例
CacheManager.shared.set("缓存值", forKey: "testKey")
let value = CacheManager.shared.get(forKey: "testKey")
print(value ?? "nil")
这里使用了:
- barrier标志确保写操作的独占性
- sync确保读操作的线程安全
- concurrent队列提高读取性能
六、替代方案:依赖注入
在某些情况下,依赖注入可能是比单例更好的选择。我们来看个例子:
protocol DatabaseServiceProtocol {
func fetchData() -> [String]
}
class DatabaseService: DatabaseServiceProtocol {
func fetchData() -> [String] {
return ["数据1", "数据2", "数据3"]
}
}
class ViewModel {
private let databaseService: DatabaseServiceProtocol
init(databaseService: DatabaseServiceProtocol) {
self.databaseService = databaseService
}
func loadData() -> [String] {
return databaseService.fetchData()
}
}
// 使用示例
let dbService = DatabaseService()
let viewModel = ViewModel(databaseService: dbService)
print(viewModel.loadData())
依赖注入的优点:
- 明确依赖关系
- 易于测试
- 更灵活的替换实现
七、总结与最佳实践
单例模式是把双刃剑,正确使用时可以简化代码,滥用则会导致各种问题。以下是我的建议:
- 严格限制单例数量:整个App最好不超过3-5个真正的单例
- 优先考虑依赖注入:特别是对于可能变化的依赖
- 提供重置机制:便于测试
- 注意线程安全:特别是可变状态的访问
- 考虑Swift的全局变量:对于简单场景,全局变量可能比单例类更合适
记住,设计模式是工具而不是目标。根据实际需求选择最合适的解决方案,而不是强迫使用某种模式。
评论