单例存在的问题

我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息 类、连接池类、ID 生成器类,但 是,这种使用方法有点类似硬编码(hard code),会带来诸多问题。

1、对 OOP 特性的支持不友好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Order {
public void create(...) {
//...
long id = IdGenerator.getInstance().getId();
//...
}
}
public class User {
public void create(...) {
// ...
long id = IdGenerator.getInstance().getId();
//...
}
}

这种涉及违背了基于接口而非实现的设计原则,如果未来某一天,我们希望针对不同的业务采用不同的 ID 生成算法。 比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Order {
public void create(...) {
//...
long id = IdGenerator.getInstance().getId();
// 需要将上面一行代码,替换为下面一行代码
long id = OrderIdGenerator.getIntance().getId();
//...
}
}
public class User {
public void create(...) {
// ...
long id = IdGenerator.getInstance().getId();
// 需要将上面一行代码,替换为下面一行代码
long id = UserIdGenerator.getIntance().getId();
}
}

除此之外,单例对继承、多态特性的支持也不友好。

2、单例会隐藏类之间的依赖关系

通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能 很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用 就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。

3、单例对代码的扩展性不友好

比如,在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据 库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。

但之后我们发现,系统 中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔 离开来执行。

为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独 享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到 其他 SQL 的执行。

4、单例对代码的可测试性不友好

如果单例类依赖比较重的外部资源,比如 DB, 我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式 的使用方式,导致无法实现 mock 替换。

而且单例往往可以理解为一个全局唯一的变量,在编写单元测试时,需要注意不同测试用例之间修改了单例的值,对测试结果的影响。

5、单例不支持有参数的构造函数

参考

《设计模式之美》