单例设计模式

2019-08-05

  这四五年的工作经验里,特别是在桌面端程序项目里,一些同事很喜欢使用单例设计模式。我感到难以理解,说实在的,很难以理解。《设计模式》一书中描述了我们使用此模式的意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。我认为这种模式的目的是最简单的,非常明确。凡是与此目的无关的需求,就不应该联想到“单例模式”。我一直以为,若不是没有办法,是绝不应该让单例模式上场的,是最后可选项。
  单例模式的滥用,无非出于如下几个需求:
1,想要通过一个统一的、简单的方式,去访问唯一的对象。
2,统一的方式,去使用单例类型的 static 能力。如,把一些工具类设计成了单例。
3,认为某个对象当前是全局唯一的,想要阻止编程者错误的第二次new 该对象。
  对于第一个需求,单例提供了最简单的全局访问方式,这并不是没有代价的。这意味着,它打破了模块边界,把我们辛辛苦苦建立起来的模块化编程的理念,瞬间就突破了。我们想要的所谓单例,在全局作用域的任何变量,不都是天然的单例嘛!怎么不把他们也搞成单例模式呢?所以,单看main函数,这里的程序结构,就天然能够保证单例。至于如何在层层嵌套的作用域中如何保证单例,更多的应该由程序结构保证,由建模保证。深层次的子作用域中,其上下文环境,甚至不应该知道某变量 是否应该 是全局单例的。这样,模块的可复用性,才能最大限度得到保证。
  对于第二个需求,则更容易剖析了。不应该把单例 与 命名空间混淆。把工具函数放在一个class内作为static method,也不是好的设计。在这里,class name也只起到了namespace 的作用,用class 反而更容易导致使用者怀疑 class 的使用方式。难道因为C++支持OOP编程,我们就不能让C++ 做面向过程编程的工作吗?像Java、Python那样,”一切都是对象“的设计哲学,已经被很多人骂了。过犹不及啊。
  对于第三个需求,这种假设其实是不靠谱的,很难以保证。一套系统里当前只使用一个传感器,谁能保证三年后不会增加第二个传感器?经验告诉我,推翻之前假设的案例绝不会少。其次,没有必要通过此种方式阻止二次new,因为系统设计的时候早就重复说明过这个问题了,错误的二次new 本应是单例的对象的人,就应该拖出去打板子。这种错误都犯,要开会何用?要文档何用?

  单例带来了三个严重的问题:
  1,打破模块边界,引入一些隐藏的依赖。架构师辛苦建立起来的框架,目标就是隔离模块,分离依赖,引入错误依赖的单例,就破坏了这些辛苦的工作。然而,单例恰巧容易错误的做到这些。新写代码一时爽,修改代码一直骂娘。大家都喜欢写新的代码,没有人喜欢修改老代码。原因何在?我一直以为,工程师的职责,不单要保证程序当前的稳定性,而且要保证三五年之内代码的可维护性。难以维护的代码,对于后续项目流程中的资金消耗的影响,不可不慎。
  2,难以单元测试。由于使用单例,则无法对模块注入该对象,无法mock。此时的单例,就是对面向接口编程原则的无情踩踏。
  3,难以子类化。并不是指设计阶段无法复用 功能类似多个class的逻辑,而是系统完成后,改版时难以修改代码。要知道,现在的软件工程,并非三十年前那种交付软件后就万事大吉的情况,而是持续迭代,很快就会轮到自己或者同事去接入新需求,修改老代码。
  那么,问题来了,哪些场景需要使用单例模式呢?
  1,即使当下全局唯一,业务核心的对象,也不适合使用单例模式。非核心的对象,如日志,监控等对象,适合单例模式。这些对象,入侵程度其实很小。
  2,程序结构上的辅助作用的全局唯一对象,适合使用单例。如factory。这些结构并不会被过多讨论,编程者可能不知是否已有全局对象可以使用。

  1. http://misko.hevery.com/2008/08/25/root-cause-of-singletons/
  2. http://web.archive.org/web/20120603233658/http://tech.puredanger.com/2007/07/03/pattern-hate-singleton
如果有任何意见,欢迎留言讨论。


[ 主页 ]
COMMENTS
POST A COMMENT

(optional)



(optional)