一、单例模式是什么鬼?

单例模式可能是设计模式家族里最出名的一个了,简单来说它就是保证一个类只有一个实例,而且提供一个全局访问点。想象一下,你家里只有一个电饭煲,全家人要用的时候都去同一个地方拿,这就是单例的精髓。

在Swift中实现单例特别简单,Swift 1.2之后有了更优雅的实现方式。先看个最简单的例子:

class MySingleton {
    // 1. 静态常量保存唯一实例
    static let shared = MySingleton()
    
    // 2. 私有化初始化方法
    private init() {}
    
    func doSomething() {
        print("单例在工作...")
    }
}

// 使用示例
MySingleton.shared.doSomething()

这个实现有几个关键点:

  1. 使用static let创建类级别的常量
  2. 将init方法设为private,防止外部创建实例
  3. 通过shared这个静态属性访问唯一实例

二、为什么我们需要单例?

单例模式在以下场景特别有用:

  1. 全局配置管理:比如App的主题设置、用户偏好等
  2. 资源共享:数据库连接池、网络请求管理器
  3. 需要频繁访问的对象:日志记录器、分析工具

举个实际的例子,我们来看一个网络请求管理器的实现:

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

这个网络管理器有几个优点:

  1. 整个App共享同一个URLSession实例
  2. 统一管理网络配置
  3. 避免重复创建URLSession带来的开销

三、单例模式的黑暗面

单例虽然好用,但滥用会导致很多问题:

  1. 全局状态难以追踪:修改单例的状态可能影响整个App
  2. 测试困难:单例很难被mock或替换
  3. 内存泄漏风险:单例生命周期与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
// ...

这种"上帝对象"式的单例会带来很多问题:

  1. 状态分散在各处修改,难以追踪
  2. 耦合度过高
  3. 难以进行单元测试

四、如何正确使用单例

为了避免滥用单例,我们可以遵循以下原则:

  1. 严格控制单例职责:一个单例应该只负责一件事
  2. 考虑依赖注入:在某些场景下,依赖注入可能是更好的选择
  3. 提供重置方法:便于测试和状态清理

改进后的用户认证管理示例:

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)
    }
    
    // ...其他实现
}

这个改进版有这些优点:

  1. 遵循单一职责原则
  2. 通过协议支持测试替身
  3. 支持依赖注入,提高可测试性

五、单例与线程安全

单例的初始化在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")

这里使用了:

  1. barrier标志确保写操作的独占性
  2. sync确保读操作的线程安全
  3. 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())

依赖注入的优点:

  1. 明确依赖关系
  2. 易于测试
  3. 更灵活的替换实现

七、总结与最佳实践

单例模式是把双刃剑,正确使用时可以简化代码,滥用则会导致各种问题。以下是我的建议:

  1. 严格限制单例数量:整个App最好不超过3-5个真正的单例
  2. 优先考虑依赖注入:特别是对于可能变化的依赖
  3. 提供重置机制:便于测试
  4. 注意线程安全:特别是可变状态的访问
  5. 考虑Swift的全局变量:对于简单场景,全局变量可能比单例类更合适

记住,设计模式是工具而不是目标。根据实际需求选择最合适的解决方案,而不是强迫使用某种模式。