最常用的专业网页设计工具,seo快速排名外包,seo概念,html5网站开发实战前言
尽管单例是一个很常用的实际模式#xff0c;在实际的开发中#xff0c;也经常使用#xff0c;但是#xff0c;有些人认为单例是一种反模式#xff08;anti-pattern#xff09;#xff0c;并不推荐使用。所以#xff0c;今天就针对这个说法详细地讲讲。
单例模式…前言
尽管单例是一个很常用的实际模式在实际的开发中也经常使用但是有些人认为单例是一种反模式anti-pattern并不推荐使用。所以今天就针对这个说法详细地讲讲。
单例模式有哪些问题为什么被称为反模式如果不用单例该如何表示全局唯一类有何替代的解决方案 单例模式有哪些问题
大部分情况下我们在项目中使用单例都是用它来表示一些全局唯一类比如配置信息类、连接池类、ID 生成器类。单例模式书写间接、使用简单在代码中我们不需要创建对象直接通过类似 IdGenerator.getInstance().getId() 这样的方法来调用就可以了。但是这种使用方法有点类似硬编码会带来诸多问题。
1. 单例对 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.getInstance().getId();// ...}
}public class User {public void create(...) {// ...long id IdGenerator.getInstance().getId();// 将上面一行替换为下面一行代码long id UserIdGenerator.getInstance().getId();// ...}
}此外单例对继承、多态的支持也不友好。一旦你选择将某个类设计成单例类也就意味着放弃了继承和多态这两个强有力的面向对象特性也就相当于损失了可以应对未来需求变化的扩展性。
2.单例会隐藏类之间的依赖关系
我们知道代码的可读性非常重要。在阅读代码的时候我们希望一眼就能看出类与类之间的依赖关系搞清楚这个类依赖了哪些外部类。
通过构造函数、参数传递等方式声明的类之间的依赖关系我们通过查看函数的定义就能很容易识别出来。但是单例类不需要显示创建、不需要依赖参数传递在函数中直接调用就可以了。如果代码比较复杂这种调用关系就会非常隐藏。在阅读代码的时候我们就需要仔细查看每个函数的代码实现才能知道这个类到底依赖了哪些单例类。
3.单例对代码的扩展性不友好
单例类只能有一个对象实例。如果未来某一天我们需要再代码中创建两个或多个实例那就要对代码有比较大的改动。 可能你会想会有这样的需求吗既然单例类大部分情况下都用来表示全局类怎么互需要两个或多个实例呢 在系统设计初期我们觉得系统中只应该有一个数据库连接池这样能方便控制数据库连接资源的消耗。所以我们把数据库连接池类设计成了单例类。但之后我们发现系统中有些 SQL 语句运行的非常慢。这些 SQL 语句在执行的时候长时间占用数据库连接资源导致其他 SQL 请求无法响应。为了解决这个问题我们希望将慢 SQL 与其他 SQL 隔离开执行。为了实现这样的目的我们可以在系统中创建两个数据库连接池慢 SQL 独享一个数据库连接池其他 SQL 独享另一个数据库连接池这样就能避免慢 SQL 影响到其他 SQL 的执行。 如果我们将数据库连接池设计成单例类显然无法适应这样的需求变更也就是说单例类在某些情况下会影响代码的扩展性、灵活性。所以数据库连接池、线程池这类的资源池最好还是不要设计成单例类。实际上一些开源的数据库连接池、线程池确实没有涉及成单例类。
4.单例对代码的可测试不友好
单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源比如 DB我们在写单元测试的时候希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式导致无法实现 mock 替换。
此外如果单例类持有成员变量比如 IdGenerator 中的 id 成员变量那它实际上相当于一种全局变量被所有的代码共享。如果这个全局变量是一个可变全局变量也就是说它的成员变量是可以被修改的那在我们编写单元测试的时候还需要注意不同测试用例之间修改了单例类中的一个成员变量的值从而导致测试结果互相影响的问题。关于这一点你可以回过头去看下《规范与重构 - 3.什么是代码的可测试性如何写出可测试性好的代码》中的 “其他场景的 Anti-Patterns全局变量” 那部分的代码示例和讲解。
5.单例不支持有参数的构造函数
单例类不支持有参的构造函数比如我们创建一个连接池的单例对象我们没法通过参数来指定连接池的大小。针对这个问题我们来看下有哪些解决方案。
第一种解决思路创建完实例后再调用 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();第二种解决思路将参数放到 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 synchronized static Singleton getInstance(int paramA, int paramB) {if (instance null) {instance new Singleton(paramA, paramB);}return instance;}
}不知道你有没有发现上面的代码其实有点问题。如果我们如下代码两次执行 getInstance() 那获取到的 singleton1 和 singleton2 的 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 final int PARAM_B 456;
}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;}
}有何替代的解决方案
为了保证全局唯一除了使用单例类还可以使用静态方法来实现。这也是项目开发中经常用到的一种实现思路。比如上一节课讲的 ID 唯一递增生成器的例子用静态方法实现以下就是下面这个样子
public class IdGenerator {private static AtomicLong id new AtomicLong(0);public static long getId() {return id.incrementAndGet();}
}// 使用举例
long id IdGenerator.getId();不过静态方法这种实现思路并不能解决之前提到的问题。实际上它比单例更加不灵活比如它无法支持懒加载。
实际上单例除了我们之前讲到的使用方法之外还有另一种使用方法。具体代码如下所示
// 1.老的使用方法
public void demoFunc() {// ...long id IdGenerator.getInstance().getId();// ...
}// 2.新的使用方式依赖注入
public void demoFunc(IdGenerator idGenerator) {// ...long id idGenerator.getId();// ...
}
// 外部调用demoFunc()时传入idGenerator
IdGenerator idGenerator IdGenerator.getInstance();
demoFunc(idGenerator);基于新的方式我们将单例生成的对象作为参数传递给函数也可以通过构造函数传递给类成员变量可以解决单例隐藏类之间依赖关系的问题。不过对于单例存在的其他问题比如对 OOP 特性、扩展性、可测试性不友好等问题还是无法解决。
所以如果要完全解决这些问题可能要从根本上寻找其他方式来实现全局唯一类。实际上类对象的全局唯一性可以通过多种不同的方式来保证。既可以通过单例模式来强制保证也可以通过工厂模式、IOC 容易比如 Spring IOC 容器来保证还可以通过程序员自己来保证自己在编写代码时保证不创建两个对象。
回顾
1.单例类存在哪些问题
单例对 OOP 特性的支持不友好。单例会隐藏类之间的依赖关系单例对代码的扩展性不友好单例对代码的可测试性不友好单例不支持有参的构造函数
2.单例有什么替代解决方案
为保证全局唯一除了使用单例还可以使用静态方法来实现。不过静态方法这种实现思路并不能解决我们之前提到的问题。如果要完全解决这些问题可能要从根上寻找其他方式来实现全局唯一类。比如通过工厂模式、IOC 容器比如 Spring IOC 容器来保证还可以通过程序员自己来保证自己在编写代码时保证不创建两个对象。
有人把单例当做反模式主张杜绝子在项目中使用。个人觉得这有点极端。模式没有对错关键看你怎么用。如果单例类并没有后续扩展的需求并且不依赖外部系统那设计成单例类是没有太大问题。对于一些全局的类我们在其他地方创建的话还要在类之间传来传去不如直接做成单例类使用起来简洁方便。