这一篇文章是对于设计模式大冒险系列的第四篇文章,这一系列的每一篇文章我都心愿可能通过通俗易懂的语言形容或者日常生活中的小例子来帮忙大家了解好每一种设计模式。明天这篇文章来跟大家一起学习一下单例模式。置信读完这篇文章之后,你必定会有所播种的。
对于单例模式,这应该是设计模式中最简略的一种了。大家如果学习过设计模式,可能很多设计模式长时间不必就遗记了,然而对于单例模式来说,你必定不会遗记。因为它的理论知识比较简单,实际起来也很不便。
然而,你真的会正确的应用单例模式吗?你晓得单例模式在什么状况下应用是适合的,什么状况下应用会造成很多麻烦吗?还是你只是把它当做一个全局变量去应用,只是因为这样开发很不便,不必写很多的代码。明天这篇文章咱们就来一起好好学习一下单例模式。让咱们开始吧。
单例模式的介绍
首先咱们先来看一下单例模式的定义是什么。所谓的单例模式,就是指对于一个具体的类来说,它有且只有一个实例,这个类负责创立惟一的实例,并且对外提供一个全局的拜访接口。
单例模式的 UML 类图能够用下图示意:
那么咱们为什么要应用单例模式呢?举一个生存中的场景,在平时你过马路的时候,给你信号提醒你能不能穿过马路的交通信号灯是不是只有一个?因为在这种状况下,如果同时有两个信号灯的话,你是不晓得该不该在此时穿过马路的。
所以类比到咱们的软件开发中,也是这么一个情理。在一个零碎中,某种用处的实例会存在惟一的一个。这个实例可能用来保留利用中的一些状态,或者执行某些工作。比方在前端开发中,咱们经常会应用一些利用的状态治理库,比方 Vuex 或者 Redux。那么在咱们的利用中,对于治理状态的实例也只能有一个,如果有多个的话就会让利用的状态呈现问题,从而导致利用产生一些谬误。
单例模式的实现
接下来咱们来看一下单例模式是如何实现的。通过下面的 UML 类图,咱们能够晓得,对于一个类来说,咱们须要一个动态变量来保留实例的援用,还须要对外提供一个获取实例的静态方法。如果应用 ES6 的类的语法来实现的话,能够简略的用上面的代码来示意:
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 指的是类,而不是实例。
上面咱们再应用函数的形式来实现一次:
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。又或者是打印日志的对象,或者是某一个性能插件等等。总之单例模式在咱们平时的开发中还是比拟常见的。
那么单例模式的劣势有哪些呢?上面简略列举了一些:
- 全局只有一个实例,提供对立的拜访与批改,保障状态性能的一致性。
- 简略、不便,容易实现。
- 提早的初始化,只有在须要的时候才去初始化对象。
单例模式的劣势
尽管单例模式的劣势很突出,然而它的毛病可是一点都不少,甚至有些开发者感觉它是反模式的。所以咱们应用单例模式的时候肯定要好好思考一下,确定是不是必须要应用单例模式。因为单例模式的不失当应用会给整个利用的测试,开发和保护带来很大的艰难。咱们接下来就来看看单例模式有哪些毛病。
单例模式的滥用会造成跟全局变量一样的一些问题
比方会减少代码的耦合性,因为单例模式全局都是能够拜访到的,那么咱们就很有可能在很多个中央应用这个惟一的对象,这样也就造成了代码的耦合。
因为程序中应用到这个单例对象的中央都能够对全局的状态进行批改,所以一旦程序在这里呈现了问题,你可能要在很多个中央进行排查,这就减少了调试和排查问题的难度。
单例模式给测试带来了很多麻烦
为什么说单例模式对测试来说是一个劫难呢?因为如果代码中应用了单例,那么咱们须要在进行代码测试的时候,提前把单例初始化好。这导致了咱们不可能在单例没有初始化好的时候对代码进行单元测试。
而且因为单例模式产生的实例只有一个,这就导致了对雷同代码进行屡次测试的时候容易呈现问题,因为实例的状态很可能在上一次测试的时候产生了扭转,从而导致了下一次测试的失败或者异样。
所以说单例模式减少了测试的难度与复杂度,减少了测试代码的工作量。
单例模式违反了软件设计的繁多职责准则
这个比拟容易了解,因为个别状况下,对于一个类来说它只负责这个类的实例具备什么性能;然而对于单例模式来说,单例模式的类还须要负责只可能产生一个实例。这违反了软件设计的繁多职责准则,类应该只负责其实例的具体性能,而不应该对类产生的实例个数负责。
然而对于这个毛病来说,大家可能会有不同的认识。不言而喻的是这样做的确更加不便,设计实现上也绝对简略一些。
单例模式暗藏了它所须要的依赖
对于个别的类来说,如果咱们的类依赖了其它的类,个别状况下,咱们能够通过类的构造函数将依赖的类显式的示意进去。这样咱们在初始化具体的类的实例的时候就晓得这个类须要那些依赖。
然而对于单例模式来说,它把它的依赖封装在外部,对于内部的使用者来说它是一个黑盒。使用者并不知道初始化这个单例须要那些依赖,所以很容易在初始化单例的时候把单例所须要的依赖忘记掉,进而导致单例初始化失败。
有时就算咱们晓得了初始化单例须要那些依赖,然而这些依赖兴许是有先后的程序的。咱们也很容易在导入和应用依赖的时候把程序搞错了,从而导致单例的初始化呈现问题。
单例模式的总结
从下面的内容咱们曾经晓得单例模式是一把双刃剑,所以你在应用的时候肯定要思考分明 。先从场景的需要上思考,是不是肯定要应用单例模式才可能解决以后的问题,有没有其它的计划。 如果肯定要应用单例模式的话,如何标准单例模式的应用,如何在程序的开发,可维护性,可拓展性以及测试的繁难性上做好均衡,是一个值得思考的问题。
文章到这里就完结了,如果大家有什么问题和疑难欢送大家在文章上面留言,或者在这里提出来。也欢送大家关注我的公众号关山不难越,获取更多对于设计模式解说的内容。
上面是这一系列的其它的文章,也欢送大家浏览,心愿大家都可能把握好这些设计模式的应用场景和解决的办法。如果这篇文章对你有所帮忙,那就点个赞,分享一下吧~
- 设计模式大冒险第三关:工厂模式,封装和解耦你的代码
- 设计模式大冒险第二关:装璜者模式,煎饼果子的主场
- 设计模式大冒险第一关:观察者模式
参考链接:
- Use your singletons wisely
- Singleton
- Singleton Pattern Pitfalls
- 单例模式
- 单例模式