Singleton是GoF设计模式之一,其易于理解,实现简单,因此得到广泛使用。但是在OOD
面向对象设计领域也有一些批评的声音,说不应该使用singleton模式,甚至有人提出
singleton是反模式(anti-pattern) 。
虾哥认为技术本身是中性的,没有生来就错的技术(inherently bad),作为工程师应该
有能力掌握技术,知道该在何时何地正确的使用何种技术。
那么下面就说说singleton的性质,以及如何避免误用singleton模式。
Singleton的性质
优势:
- 作为静态成员,可以在程序不同位置”便捷地”调用一个共享的对象。
- 共享的对象有且只有一个实例。
- 共享的对象是一个类的实例,可以使用面向对象方法进行设计,如实现接口。
劣势:
- Singleton自身既包含行为的实现,又包含其自身的创建,违反了单一职责原则。
- Singleton作为全局对象,可能会隐藏一些依赖关系,如需要按指定顺序访问全局对象的两个
对象实际上是有依赖的。 - 直接的依赖会构成紧耦合,导致很难进行模拟测试(stub or mock)
为什么需要使用singleton模式
插图:面向对象的生命周期树
基于面向对象思想编写的程序,其运行过程中实际上就是各种对象的生命周期不断新陈代谢的过程。
每个对象都有负责构建它的另一个对象,因此每个对象实例的生命周期可以看作层次结构,可以表现为树形。
有的时候我们需要在树的不同位置共享同一个对象实例,用纯面向对象的方式,我们需要找到两个对象的共同祖先,
在祖先对象上构造需要共享的对象,并逐层传递到子孙对象上。但是如果层次结构比较深,这种方式就过于繁琐了。
想象一下你需要给程序中每个对象都传递一个Logger对象。
插图:传递一个对象引用
这种情况下我们可以构造另外一棵树,他的最原始祖先是一个全局变量或者静态成员(即它的生命周期不被其他对象拥有)。
这时我们可以方便的从一棵对象生命周期树的不同节点同时访问另一棵树,即可完成对象的共享。
这种模式在框架设计上非常常见,即提供统一的静态入口点访问某一框架的具体功能,例如log4net中的LogManager对象。
如何消除或减弱singleton 的不利影响
我们假设实际的场景中类之间的关系是这样的
这其中有两个依赖关系,即 A类依赖于Singleton类,Singleton类依赖于B类。这种类和类之间的直接依赖违反了dependency inversion原则,造成了紧密的耦合。
可以使用以下一种或几种方式共用来降低负面影响。
依赖反转
A和Singleton之间的关系可以通过依赖反转来解决。
A类只依赖于ISingleton的约定而非Singleton类的具体实现。
修改前
修改后
依赖注入 Dependency Injection
让使用者(client)被注入一个其依赖的对象的实例(service),而不是让它自行构建或获取service的实例。
public class A
{
private ISingleton singleton;
public A(ISingleton singleton)
{
this.singleton = singleton;
}
public void Foo()
{
this.singleton.Bar()
}
}
不变对象 Immutable
如果全局对象的写入和读取在顺序上有隐含条件,则该读取操作和写入操作实际上是有依赖关系的。
更糟糕的是这种依赖关系没有暴露出来造成不易察觉的设计问题。
提供一个显式或隐式的初始化过程,在其生命周期中保持状态不会变化
,确保总是有
可预期的调用,以便支持单元测试。
隐式初始化应从一个指定位置,如环境变量,某个(或多个)具体的配置文件路径,加载构造内部对象所需的信息。
则单元测试时可以通过间接的方式修改构造过程注入所需的mock对象。
参见log4net config
https://logging.apache.org/log4net/release/manual/configuration.html
参考
https://en.wikipedia.org/wiki/Dependency_inversion_principle
https://en.wikipedia.org/wiki/Dependency_injection
https://fuzhe1989.github.io/2017/09/30/why-global-static-singleton-var-evil/
https://www.dre.vanderbilt.edu/~schmidt/PDF/Context-Object-Pattern.pdf