创立型:单例设计不敌对
目录介绍
- 01. 前沿简略介绍
- 02. 单例对 OOP 不敌对
- 03. 暗藏类之间依赖
- 04. 代码扩展性不敌对
- 05. 可测试性不敌对
- 06. 不反对有参构造函数
- 07. 有何代替解决方案
01. 前沿简略介绍
- 只管单例是一个很罕用的设计模式,在理论的开发中,咱们也的确常常用到它,然而,有些人认为单例是一种反模式(anti-pattern),并不举荐应用。
- 所以,就针对这个说法具体地讲讲这几个问题:单例这种设计模式存在哪些问题?为什么会被称为反模式?如果不必单例,该如何示意全局惟一类?有何代替的解决方案?
02. 单例对 OOP 不敌对
-
OOP 的四大个性是封装、形象、继承、多态。单例这种设计模式对于其中的形象、继承、多态都反对得不好。为什么这么说呢?咱们还是通过 IdGenerator 这个例子来解说。
public class Order {public void create(...) { //... long id = IdGenerator.getInstance().getId(); //... } } public class User {public void create(...) { // ... long id = IdGenerator.getInstance().getId(); //... } }
-
IdGenerator 的应用形式违反了基于接口而非实现的设计准则,也就违反了狭义上了解的 OOP 的形象个性。如果将来某一天,咱们心愿针对不同的业务采纳不同的 ID 生成算法。比方,订单 ID 和用户 ID 采纳不同的 ID 生成器来生成。为了应答这个需要变动,咱们须要批改所有用到 IdGenerator 类的中央,这样代码的改变就会比拟大。
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(); } }
- 除此之外,单例对继承、多态个性的反对也不敌对。这里之所以会用“不敌对”这个词,而非“齐全不反对”,是因为从实践上来讲,单例类也能够被继承、也能够实现多态,只是实现起来会十分奇怪,会导致代码的可读性变差。不明确设计用意的人,看到这样的设计,会感觉莫名其妙。所以,一旦你抉择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象个性,也就相当于损失了能够应答将来需要变动的扩展性。
03. 暗藏类之间依赖
- 代码的可读性十分重要。在浏览代码的时候,咱们心愿一眼就能看出类与类之间的依赖关系,搞清楚这个类依赖了哪些外部类。通过构造函数、参数传递等形式申明的类之间的依赖关系,咱们通过查看函数的定义,就能很容易辨认进去。然而,单例类不须要显示创立、不须要依赖参数传递,在函数中间接调用就能够了。如果代码比较复杂,这种调用关系就会十分荫蔽。在浏览代码的时候,咱们就须要认真查看每个函数的代码实现,能力晓得这个类到底依赖了哪些单例类。
04. 代码扩展性不敌对
- 单例类只能有一个对象实例。如果将来某一天,咱们须要在代码中创立两个实例或多个实例,那就要对代码有比拟大的改变。你可能会说,会有这样的需要吗?既然单例类大部分状况下都用来示意全局类,怎么会须要两个或者多个实例呢?
- 实际上,这样的需要并不少见。咱们拿数据库连接池来举例解释一下。
- 在零碎设计初期,咱们感觉零碎中只应该有一个数据库连接池,这样能不便咱们管制对数据库连贯资源的耗费。所以,咱们把数据库连接池类设计成了单例类。但之后咱们发现,零碎中有些 SQL 语句运行得十分慢。这些 SQL 语句在执行的时候,长时间占用数据库连贯资源,导致其余 SQL 申请无奈响应。为了解决这个问题,咱们心愿将慢 SQL 与其余 SQL 隔离开来执行。为了实现这样的目标,咱们能够在零碎中创立两个数据库连接池,慢 SQL 独享一个数据库连接池,其余 SQL 独享另外一个数据库连接池,这样就能防止慢 SQL 影响到其余 SQL 的执行。
- 如果咱们将数据库连接池设计成单例类,显然就无奈适应这样的需要变更,也就是说,单例类在某些状况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也的确没有设计成单例类。
05. 可测试性不敌对
- 单例模式的应用会影响到代码的可测试性。如果单例类依赖比拟重的内部资源,比方 DB,咱们在写单元测试的时候,心愿能通过 mock 的形式将它替换掉。而单例类这种硬编码式的应用形式,导致无奈实现 mock 替换。
- 除此之外,如果单例类持有成员变量(比方 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是能够被批改的,那咱们在编写单元测试的时候,还须要留神不同测试用例之间,批改了单例类中的同一个成员变量的值,从而导致测试后果相互影响的问题。
06. 不反对有参构造函数
- 单例不反对有参数的构造函数,比方咱们创立一个连接池的单例对象,咱们没法通过参数来指定连接池的大小。针对这个问题,咱们来看下都有哪些解决方案。
-
第一种解决思路是:创立完实例之后,再调用 init() 函数传递参数。须要留神的是,咱们在应用这个单例类的时候,要先调用 init() 办法,而后能力调用 getInstance() 办法,否则代码会抛出异样。具体的代码实现如下所示:
public class Singleton { private static Singleton instance = null; private final int paramA; private final int paramB; private Singleton(int paramA, int paramB) { this.paramA = paramA; this.paramB = paramB; } public static Singleton getInstance() {if (instance == null) {throw new RuntimeException("Run init() first."); } return instance; } public synchronized static Singleton init(int paramA, int paramB) {if (instance != null){throw new RuntimeException("Singleton has been created!"); } instance = new Singleton(paramA, paramB); return instance; } } Singleton.init(10, 50); // 先 init,再应用 Singleton singleton = Singleton.getInstance();
-
第二种解决思路是:将参数放到 getIntance() 办法中。具体的代码实现如下所示:
public class Singleton { private static Singleton instance = null; private final int paramA; private final int paramB; private Singleton(int paramA, int paramB) { this.paramA = paramA; this.paramB = paramB; } public synchronized static Singleton getInstance(int paramA, int paramB) {if (instance == null) {instance = new Singleton(paramA, paramB); } return instance; } } Singleton singleton = Singleton.getInstance(10, 50);
-
不晓得你有没有发现,下面的代码实现略微有点问题。如果咱们如下两次执行 getInstance() 办法,那获取到的 singleton1 和 signleton2 的 paramA 和 paramB 都是 10 和 50。也就是说,第二次的参数(20,30)没有起作用,而构建的过程也没有给与提醒,这样就会误导用户。这个问题如何解决呢?
Singleton singleton1 = Singleton.getInstance(10, 50); Singleton singleton2 = Singleton.getInstance(20, 30);
-
-
第三种解决思路是:将参数放到另外一个全局变量中。具体的代码实现如下。Config 是一个存储了 paramA 和 paramB 值的全局变量。外面的值既能够像上面的代码那样通过动态常量来定义,也能够从配置文件中加载失去。实际上,这种形式是最值得举荐的。
public class Config { public static final int PARAM_A = 123; public static fianl int PARAM_B = 245; } public class Singleton { private static Singleton instance = null; private final int paramA; private final int paramB; private Singleton() { this.paramA = Config.PARAM_A; this.paramB = Config.PARAM_B; } public synchronized static Singleton getInstance() {if (instance == null) {instance = new Singleton(); } return instance; } }
07. 有何代替解决方案
- 提到了单例的很多问题,你可能会说,即使单例有这么多问题,但我不必不行啊。业务上有示意全局惟一类的需要,如果不必单例,怎么能力保障这个类的对象全局惟一呢?
-
为了保障全局惟一,除了应用单例,咱们还能够用静态方法来实现。这也是我的项目开发中常常用到的一种实现思路。比方,上一节课中讲的 ID 惟一递增生成器的例子,用静态方法实现一下,就是上面这个样子:
// 静态方法实现形式 public class IdGenerator {private static AtomicLong id = new AtomicLong(0); public static long getId() {return id.incrementAndGet(); } } // 应用举例 long id = IdGenerator.getId();
-
不过,静态方法这种实现思路,并不能解决咱们之前提到的问题。实际上,它比单例更加不灵便,比方,它无奈反对提早加载。咱们再来看看有没有其余方法。实际上,单例除了咱们之前讲到的应用办法之外,还有另外一个种应用办法。具体的代码如下所示:
// 1. 老的应用形式 public demofunction() { //... long id = IdGenerator.getInstance().getId(); //... } // 2. 新的应用形式:依赖注入 public demofunction(IdGenerator idGenerator) {long id = idGenerator.getId(); } // 内部调用 demofunction() 的时候,传入 idGenerator IdGenerator idGenerator = IdGenerator.getInsance(); demofunction(idGenerator);
- 基于新的应用形式,咱们将单例生成的对象,作为参数传递给函数(也能够通过构造函数传递给类的成员变量),能够解决单例暗藏类之间依赖关系的问题。不过,对于单例存在的其余问题,比方对 OOP 个性、扩展性、可测性不敌对等问题,还是无奈解决。
更多内容
- GitHub:https://github.com/yangchong211
- 博客:https://juejin.cn/user/197877…
- 博客汇总:https://github.com/yangchong2…