这一篇文章是关于设计模式大冒险系列的第四篇文章,这一系列的每一篇文章我都但愿可以经过通俗易懂的语言描述或者平常生活中的小例子来帮助你们理解好每一种设计模式。今天这篇文章来跟你们一块儿学习一下单例模式。相信读完这篇文章以后,你确定会有所收获的。javascript
关于单例模式,这应该是设计模式中最简单的一种了。你们若是学习过设计模式,可能不少设计模式长时间不用就忘记了,可是对于单例模式来讲,你确定不会忘记。由于它的理论知识比较简单,实践起来也很方便。html
可是,你真的会正确的使用单例模式吗?你知道单例模式在什么状况下使用是合适的,什么状况下使用会形成不少麻烦吗?仍是你只是把它当作一个全局变量去使用,只是由于这样开发很方便,不用写不少的代码。今天这篇文章咱们就来一块儿好好学习一下单例模式。让咱们开始吧。前端
首先咱们先来看一下单例模式的定义是什么。所谓的单例模式,就是指对于一个具体的类来讲,它有且只有一个实例,这个类负责建立惟一的实例,而且对外提供一个全局的访问接口。vue
单例模式的UML类图能够用下图表示:java
那么咱们为何要使用单例模式呢?举一个生活中的场景,在平时你过马路的时候,给你信号提示你能不能穿过马路的交通讯号灯是否是只有一个?由于在这种状况下,若是同时有两个信号灯的话,你是不知道该不应在此时穿过马路的。git
因此类比到咱们的软件开发中,也是这么一个道理。在一个系统中,某种用途的实例会存在惟一的一个。这个实例可能用来保存应用中的一些状态,或者执行某些任务。好比在前端开发中,咱们经常会使用一些应用的状态管理库,好比Vuex或者Redux。那么在咱们的应用中,对于管理状态的实例也只能有一个,若是有多个的话就会让应用的状态出现问题,从而致使应用发生一些错误。github
接下来咱们来看一下单例模式是如何实现的。经过上面的UML类图,咱们能够知道,对于一个类来讲,咱们须要一个静态变量来保存实例的引用,还须要对外提供一个获取实例的静态方法。若是使用 ES6 的类的语法来实现的话,能够简单的用下面的代码来表示:web
class Singleton { // 类的静态属性 static instance = null; // 类的静态方法 static getInstance() { if (this.instance === null) { this.instance = new Singleton(); } return this.instance; } } const a = Singleton.getInstance(); const b = Singleton.getInstance(); console.log(a === b); // true
上面的代码仍是比较简单的,相信你们看一下就知道怎么实现了。须要注意的一点是,在类的静态方法中,this指的是类,而不是实例。vuex
下面咱们再使用函数的方式来实现一次:redux
const Singleton = (function() { let instance; // 初始化单例对象的方法 function initInstance() { return {}; } return { getInstance() { if (instance === null) { instance = initInstance(); } return instance; }, }; })(); const a = Singleton.getInstance(); const b = Singleton.getInstance(); console.log(a === b); // true
上面这两种方法的实现都是差很少的,你能够根据本身的喜爱选择不一样的实现方式。
做为Web前端开发者来讲,由于咱们使用的开发语言基本上是JavaScript,又由于JavaScript是一种单线程语言,因此咱们通常不会遇到在多线程环境中使用单例模式会遇到的一些问题。
那么咱们若是在多线程的环境中使用单例模式须要注意什么呢?首先在单例尚未初始化的时候,若是有多个线程访问建立单例模式的代码,在没有作额外处理的状况下,就有可能会建立多个单例。
固然也有解决的方法,一种方法就是咱们在类初始化的时候就把单例生成了,这样之后经过获取单例的接口获取到的都是最开始生成的那个单例。可是这样就失去了延时初始化单例的好处。若是单例的初始化须要花费的资源或者时间比较少,这种方法是能够的。反之,这样作有就有一些浪费了。由于可能在整个应用的运行过程当中,这个单例一次也没有被使用过。
另外一种方式就是在建立单例的时候须要加锁,保证同时只能有一个线程在建立单例。这样的话咱们就保证了建立的单例是惟一的。固然具体的操做还跟实现单例模式选择的语言有关系,这里就不在深刻讨论了。
单例模式适合用在这样的场景中:系统中须要一个惟一的对象去控制、管理和分享系统的状态,或者执行某一个特定的任务又或是实现某一个具体的功能。在咱们的前端开发中,最多见的就是应用的状态管理对象,好比 Vuex 和 Redux。又或者是打印日志的对象,或者是某一个功能插件等等。总之单例模式在咱们平时的开发中仍是比较常见的。
那么单例模式的优点有哪些呢?下面简单列举了一些:
虽然单例模式的优点很突出,可是它的缺点但是一点都很多,甚至有些开发者以为它是反模式的。因此咱们使用单例模式的时候必定要好好思考一下,肯定是否是必需要使用单例模式。由于单例模式的不恰当使用会给整个应用的测试,开发和维护带来很大的困难。咱们接下来就来看看单例模式有哪些缺点。
好比会增长代码的耦合性,由于单例模式全局都是能够访问到的,那么咱们就颇有可能在不少个地方使用这个惟一的对象,这样也就形成了代码的耦合。
由于程序中使用到这个单例对象的地方均可以对全局的状态进行修改,因此一旦程序在这里出现了问题,你可能要在不少个地方进行排查,这就增长了调试和排查问题的难度。
为何说单例模式对测试来讲是一个灾难呢?由于若是代码中使用了单例,那么咱们须要在进行代码测试的时候,提早把单例初始化好。这致使了咱们不可以在单例没有初始化好的时候对代码进行单元测试。
并且由于单例模式产生的实例只有一个,这就致使了对相同代码进行屡次测试的时候容易出现问题,由于实例的状态极可能在上一次测试的时候发生了改变,从而致使了下一次测试的失败或者异常。
因此说单例模式增长了测试的难度与复杂度,增长了测试代码的工做量。
这个比较容易理解,由于通常状况下,对于一个类来讲它只负责这个类的实例具备什么功能;可是对于单例模式来讲,单例模式的类还须要负责只可以产生一个实例。这违背了软件设计的单一职责原则,类应该只负责其实例的具体功能,而不该该对类产生的实例个数负责。
可是对于这个缺点来讲,你们可能会有不一样的见解。显而易见的是这样作确实更加方便,设计实现上也相对简单一些。
对于通常的类来讲,若是咱们的类依赖了其它的类,通常状况下,咱们能够经过类的构造函数将依赖的类显式的表示出来。这样咱们在初始化具体的类的实例的时候就知道这个类须要那些依赖。
可是对于单例模式来讲,它把它的依赖封装在内部,对于外部的使用者来讲它是一个黑盒。使用者并不知道初始化这个单例须要那些依赖,因此很容易在初始化单例的时候把单例所须要的依赖忘记掉,进而致使单例初始化失败。
有时就算咱们知道了初始化单例须要那些依赖,可是这些依赖也许是有前后的顺序的。咱们也很容易在导入和使用依赖的时候把顺序搞错了,从而致使单例的初始化出现问题。
从上面的内容咱们已经知道单例模式是一把双刃剑,因此你在使用的时候必定要考虑清楚。先从场景的需求上考虑,是否是必定要使用单例模式才可以解决当前的问题,有没有其它的方案。若是必定要使用单例模式的话,如何规范单例模式的使用,如何在程序的开发,可维护性,可拓展性以及测试的简易性上作好平衡,是一个值得考虑的问题。
文章到这里就结束了,若是你们有什么问题和疑问欢迎你们在文章下面留言,或者在这里提出来。也欢迎你们关注个人公众号关山不难越,获取更多关于设计模式讲解的内容。
下面是这一系列的其它的文章,也欢迎你们阅读,但愿你们都可以掌握好这些设计模式的使用场景和解决的方法。若是这篇文章对你有所帮助,那就点个赞,分享一下吧~
参考连接: