深一层看Singleton
Singleton模式通常被认为是比较容易理解和运用的设计模式。目前,网上已经有相当多的资料讲解Singleton的基础知识,本文试图避免重复性的介绍,而是尝试从不同的角度更全面地探讨Singleton。
“保证对象有唯一的实例,并且提供一个全局访问点”是Singleton模式比较常见的描述。不知您是否意识到,这个定义本身就散发着某种"bad smell"。为什么要把“保证对象有唯一实例”的创建职责,和“提供一个全局访问点”的访问职责混入一个模式呢?
为了更清楚地说明这个问题,我们假设有一个Singleton的类A,并通过静态方法A.Instance()提供唯一实例。使用者类B通常会直接以 A.Intance().Do()的方式来使用它(因为这样很方便)。这里我们需要追问的是B的设计,“B一定要A的单例对象才行吗?不是单例可以吗?”。其实,绝大多数情况B所需要的仅仅是A的对象,它甚至根本不需要知道“单例”为何物。但不幸的是以方便的理由,“单例”成功地混入了B,这就造成 B对A的“单例耦合”。"单例耦合"直接损害了B的可测试性,因为我们无法对类A进行mock,只能带着A测试B,扩大了测试边界。一般来讲,具有好的可测试性的模块,通常表现为依赖注入的开放式设计,因为这样才方便通过mock等方式模拟外部依赖,让测试专注于模块本身的逻辑,即测试边界只包含被测模块本身。
通过上面的分析,问题已经很清楚了,B是A的使用者,它只关注A提供的功能接口;A只能有唯一实例是A的创建者的事情,与B无关。所以,我们提倡B应该采用开放式的依赖注入,比如通过构造函数或者通过Setter让外部注入A的实例,不要仅仅因为方便而滥用Singleton。对于创建唯一实例的职责可以采用传统 Singleton模式,也可以采用其它方式。从理论上讲,创建A的职责不一定要放在A内部来实现,我们完全可以结合工厂模式,设计所谓的单例工厂,保证从这个工厂出品的对象是唯一实例。
其实,Singleton更本质的东西不是“保证类有唯一对象”而是“保证类对象有唯一的状态”(修正:这种说法有偏颇之处,某些情况下保证引用同一性是建模和语义的要求)。这里涉及两个问题:
第一“既然是类,又是唯一,为什么不干脆弄成static算了,还要Singleton模式干嘛?”。其实,这是为了避免使用者B对类A的直接依赖,采用 static就无法把B设计成依赖注入式的,采用static意味着失去了多态。这里的多态特别指对Singleton类本身有接口要求,那么 static设计就直接被排除在外了。
第二“既然用户不关心是一个对象还是多个对象,是否可能类有多个对象,但都共享同一状态,用这种方式实现状态唯一性需求?”。答案是肯定的,MonoState模式正是采用这种方式,把唯一状态通过static成员封装在类内部,让所有对象共享同一状态。与Singleton相比,MonoState更好地分离了状态唯一性和对象使用,避免出现耦合。
更多关于Singleton模式的探讨,可以参考: