Resist the Temptation of the Singleton Pattern
单例模式可以解决你的很多问题。你会认为只要单例模式就够了。这可以保证实例在初始化之前就能使用。通过一个全局指针访问就能保持你的设计简洁。这么经典的设计模式还有什么不对的地方吗?
其实太多了。它们可能很有诱惑力,但根据经验来看,其所带来的损害要大过好处。它们阻碍了可测试性和可维护性。不幸的是,这种认识应该还没有得到广泛的传播,单例模式仍然吸引住广大程序员中的。但它们值得被抵抗:
- 单例需求是被幻想出来的。很多情况下,它只是对未来不会再有新增实例的纯粹推测。把这种推测贯穿到整个应用程序设计势必会在某些地方引起痛苦。需求总会变。好的设计拥抱这一点,单例模式不会。
- 单例导致概念上独立的代码单元之间存在隐式依赖。所造成的问题是单例在单元之间是隐藏的,并引入了不必要的耦合。当你尝试写个松耦合的单元测试有选择性地来模拟真实情况的时候,这种代码闻起来相当刺鼻。单例简单粗暴地阻碍了这种模拟。
- 单例还会带有隐式的持久状态,再次阻挠了单元测试。单元测试是彼此独立了,因此测试可以在任何情况下运行,程序在每次测试执行前也可以设置为一个已知的状态。一旦你在可变状态下引入单例,就很难达到效果。此外,这种全局访问的持久状态将会使代码变得更难以查找问题,尤其是在多线程环境。
- 多线程进一步加大了单例模式的陷阱。简单的加锁访问不是很有效率,被称作双重验证锁的模式(DCLP)广受欢迎。不幸的是,这可能是一种致命吸引力的形式,不论在哪里,都仍然有可能弄巧成拙。
清除单例也存在着最终的挑战:
- 不支持显式杀死单例。在某些情况下这是个很严重的问题,例如,在插件架构中,一个插件只有在所有对象都被清理干净后才能被安全写在。
- 不支持在程序退出后隐式清理。对于含有相互依存的单例将会非常麻烦。当应用程序关闭时,某个单例可能会去访问其他已经被销毁的(对象)。
这其中的一些缺陷可通过引入额外的机制来解决。但无不如何,这样做的代价是增加了代码本可以用其他设计来避免的复杂度。
所以,把你使用的单例模式限制在永远不会实例化超过一次的类上。不要在含糊不清的代码里使用单个全局访问指针。相反,对单例的直接访问应该来自几个定义明确的地方,可以通过接口传递给其他代码。这里的其他代码是不知道它的,不需要依赖单例或其他类的实现接口。这就破除了单元测试的依赖性,并提高了可维护性。所以,下次你想要实现和访问单例的时候,我希望你能三思而后行。