[Swift设计模式] 单例

987 阅读5分钟
更多内容,欢迎关注公众号: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及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~