[上一节]我们主要学习了使用设计模式来写代码的指导思想以及设计模式的分门别类,本节主要学习创建型的三种设计模式是怎么使用的。如何利用创建型设计模式来指导我们更好的封装代码更好的创建对象。
为什么要封装?封装能带给我们什么好处?
什么是好的封装?
总结起来一共六个字:不可见、留接口
我们要介绍三种帮助我们封装对象的设计模式,这三种设计模式能非常有效的帮助我们产出对象。为什么要在这里把对象和封装放在一起?原因是应用设计模式在产出对象的时候,其实已经达成了一种比较好的封装。
工厂模式:既然叫做工厂,其实就是创造一个专门用来创建对象的工厂,我们通过这个工厂来获取对象,从而代替我们使用new操作符或者是代替我们通过手写来生成对象。
目的:方便我们大量创建对象。
应用场景:工厂肯定是要大批量的产出某样东西,所以说当你需要创建的对象需要大批量生成时,应该首先想到工厂模式。比如我们项目中有很多的弹窗,我们应该把我们弹窗组件封装成工厂模式提供给别人使用,当别人需要调用弹窗的时候只需要调用工厂就能拿到弹窗对象了。
建造者模式:工厂模式用来大量产出类似对象,这些对象可能只是内容不同,其它部分大致相同,而建造者模式截然相反,建造者我们首先想到造房子,造房子肯定是精细化建造,不可能大批量产出房子,所以说建造者模式其实是一种精细化构建的思想。
目的:指导我们通过组合构建一个复杂的全局对象
应用场景:当需要创建单个、庞大的可能并不需要大量产出的组合对象时,就可以考虑使用建造者模式,它与工厂模式分别对应于编程中产出对象的两个方面。例如:写一个复杂的轮播图,轮播图一般一个页面只有一个,而轮播图有各种各样的动画效果以及各种复杂的交互,此时采用建造者模式封装代码是非常好的。
以上是两个帮助我们创建对象的设计模式,在创建完对象之后往往需要一些特殊性的需求,例如:需要保障对象在全局有且仅有一个
单例模式:为什么要确保全局有且只有一个对象呢?因为在很多情况下,我们为了避免出现多个对个对象之间发生干扰,此时我们往往需要保证全局只有一个对象。
目的:确保全局有且仅有一个对象
应用场景:为了避免重复新建,避免多个对象存在互相干扰。例如:当在项目中重复的去new同一个类,会导致多个对象之间存在干扰,这个时候可以考虑使用单例模式。
工厂模式就是写一个方法,只需要调用这个方法,就能拿到你要的对象,例如:
Factory方法就是工厂模式要写的工厂,工厂要做的事情非常简单,通过参数(本例中为type)告诉工厂我要什么样的对象,在工厂内部根据参数判断要什么对象,然后在内部创建好对象并返回。
建造者模式是把一个复杂的类各个部分拆分成独立的类,然后再在最终类里组合到一块,例如:
建造者模式突出建造,像我们造房子一样,我们会用预制好的板、梁这些预制好的东西来造成我们的房子,因为建造者模式的内部结构比较复杂,所以在写建造者模式的时候它的内部会有很多别的类组成,就像上面代码所写的Model1、Model2,可以把它们看做建房子的板和梁,最终拿出去给别人使用的类会由Model1、Model2在内部组合而成,也就是我们说的把一个复杂的类拆分成独立的类,然后再组合到一起形成最终使用的类,Final为最终给出去的类。
单例模式是通过定义一个方法,使用时只允许通过此方法拿到存在内部的同一实例化对象,例如:
单例模式的做法并不是很固定,更重要的是要记住它全局只有一个对象的思想,例如代码示例中的Singleton就是一个作为单例来实例化的一个对象,Singleton对象下挂载一个getInstance方法,只能通过这个方法来获取这个类的实例化对象。这个方法里面先判断this上有没有instance属性,如果有直接返回这个属性,如果没有就把这个属性赋值为实例化的Singleton并返回。这样我们通过调用getInstance方法来拿到实例对象,如果已经实例化过了就会拿到之前实例化的对象,如果没有实例化过,就会把这个类实例化。
要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
需求:项目中有一个弹窗需求,弹窗有多种,它们之间存在内容和颜色上的差异。
假如我们有一个info弹窗、一个confirm弹窗、一个cancel弹窗
如果我们需要创建3个info弹窗、三个confirm弹窗、三个cancel弹窗,分别有不同的内容和颜色,在没有工厂模式的情况下,我们很有可能会这样做:创建三个Info弹窗,new infoPop然后传入内容和颜色,然后复制粘贴一大堆,这样写就很麻烦。如:
我们用工厂模式改造一下
创建一个pop工厂,然后传入弹窗类型,内容和颜色参数,在工厂里面判断要什么类型的弹窗就返回什么类型的弹窗。这样创建工厂之后我们再去创建弹窗对象只需要告诉工厂需要什么类型的弹窗即可。
代码写成这样之后就可以把这部分代码作为插件的代码封装起来,封装很简单,把代码放入匿名自执行函数中,然后指向外部暴露工厂即可(挂载在全局对象下),外部的使用者就可以直接调用工厂而不用关心具体要new哪个弹窗。
如果有很多个弹窗就可以把弹窗配置成数组,数组中定好要什么弹窗,弹窗的内容和颜色,然后循环数组调用工厂即可。
上面的弹窗工厂还存在一个问题,如果使用者在不知道的情况下去new了这个工厂,这样可能就不太好使了,所以我们改造一下工厂,判断this是不是弹窗工厂pop,如果是就代表使用者使用了new操作符,此时就给它new一个实例,实例为this[type](content, color),如果不是就代表使用者是直接调用的,就去new一下弹窗工厂pop,让它再走到流程中去。
此时就不用switch去进行判断了,可以把switch代码都删掉,把infoPop、confirmPop、cancelPop这些方法都挂载到pop工厂的prototype上面去。
这样无论以后再要扩展不同类型的弹窗也好,还是需要减少弹窗也好都会方便很多,因为我们只需要修改一下原型链即可,而对于之前这种switch的写法,我们需要修改一下switch还要加一层判断,换成这种写法我们只需要修改prototype就可以了。这种就是加了健壮性判断和可扩展性的工厂代码。
需求:jQuery需要操作dom,每一个dom都是一个jQuery对象。
jQuery把dom包装成jQuery对象来方便我们操作dom,我们要操作这么多dom,如果每一个jQuery对象都需要new的话,这样的操作就十分麻烦,所以说jQuery本身的封装采用的是工厂模式,我们只需要调用jQuery的$方法就能拿到jQuery对象,jQuery的源码内部是怎么构造这个工厂的?
首先在外层以一个匿名自执行函数将代码封装起来,然后在自执行函数内部定义一个暴露出去的jQuery方法,它接收一个选择器以及上下文,再把jQuery方法作为一个$符号和jQuery挂载到window对象上,也就是把工厂挂载到window上。
jQuery工厂内部返回了一个jQuery.fn下的init方法,也就是说其实拿到的对象是通过jQuery.fn.init创建的实例对象,这里之所以不去new jQuery类本身,是因为会形成无限循环的递归。
然后让jQuery.fn等于jQuery.prototype,因为所有的方法都会挂载到prototype下,这里利用引用的特点让fn等于prototype,引用了prototype上所有的方法。然后在prototype下创建init方法。
既然我们最终拿到的实例化对象是init的实例化对象,所以说init类的原型链要和jQuery本身的原型链等价,所以再把jQuery.fn.init.prototype等于jQuery.fn,也就相当于等于jQuery的prototype
相对于jQuery的整个架构而言,它的各种各样的方法和模块是怎么扩展的呢?
它有一个extend方法,这个方法的作用是拷贝,如果只传一个对象它会把这个对象拷贝到jQuery上面
代码示例中拷贝到jQuery的fn上面也相当于拷贝到它的原型链上面,也相当于拷贝到了init的原型链上面,然后各种各样的模块就可以通过extend拷贝进去。比如css方法、animate方法等等都会通过这个方法拷贝进去。
以上就是jQuery架构上的实现,其实就是一个工厂模式,只不过它去构造工厂的方式和我们前面演示的代码有点差别,但它始终逃不出一点就是用调用方法的方式自动给我们想要的对象来代替我们去new想要的对象。这样带来的好处第一个就是方便我们操作,像jQuery中大量操作dom的情况下没必要一个个去new它,第二个就是我们可以没必要去具体了解要new哪个,只需要告诉工厂需要哪个就可以了。
通过以上两个案例,不难发现工厂模式就是把真正需要暴露的对象先封装起来,然后只暴露一个工厂方法,让使用者通过这个工厂方法来获取对象,它的优势在于方便我们大量创建对象。
需求:有一个编辑器插件,初始化的时候需要配置大量参数,而且内部功能很多。(搭建架子,细节不实现)
编辑器插件的功能很多,比如我们可以前进后退、编辑字体颜色等等功能,面对这样一个复杂的编辑器插件,我们使用建造者模式是非常合适的,因为我们要写的编辑器插件:
编辑器会有一个最终的类,也就是说使用的时候需要new这个类(本例为Editor类),建造者模式是把它的模块拆分成独立的类然后再组合起来,要完成我们的功能,需要拆分出哪些类?
模块都拆出来之后,再去给这些类定义方法
我们把最终给别人使用的类Editor挂载到window对象上
然后将拆分出来的类在Editor类中组合起来
当我们把这些独立的模块都构建进Editor类之后,这些模块就都可以互相调用了。比如我们现在增加一个状态回滚的功能,我们只需要拿出当前状态。
首先在stateControll类中定义一个state数组专门用来存储前进以及后退的状态,然后创建状态指针,指向当前的状态
在状态回滚方法stateBack中,只需要从状态数组里面取出当前状态的上一个状态,然后调用字体控制模块fontControll改变字体
在这样的一种组织之下,各种模块之间的沟通会变得很明晰,一个复杂功能的插件被我们解析成了几个独立的小插件,最后再组合起来,这样既方便我们编写代码,也方便我们去组织模块之间的沟通。保持了模块间的低耦合,从而形成高效沟通和高效编程,这就是建造者模式的目的。
需求:vue内部模块众多,而且过程复杂,有需要可以自己去阅读源码。
Vue内部的建造方式也可以看成是建造者模式,它的建造过程如下:
首先创建Vue类,为了防止使用者不通过new操作符调用它,需要在它的内部使用instance进行判断this是不是Vue,如果不是代表没有使用new操作符,此时需要抛出警告告诉使用者没有使用new操作符,如果是则调用init方法,将用户在初始化的时候后传入的配置参数options传进去完成一个配置的初始化。
vue相关的很多功能比如生命周期、事件系统、渲染函数、都是怎么注入到这个极其简单的Vue类中的呢?
vue源码中调用了一系列的初始化方法进行混入,例如:
通过调用这些方法混入到Vue类,和上例中将模块独立为一个个类最后放到Editor类中是一样的道理,只不过Vue中将写法改成了方法调用,而上例中直接在构造函数中写入。
Vue类本身非常简单,它所有的功能都是独立开发然后通过一系列的混入完成的,由此可见Vue使用的也是建造者模式来构建对象的。
需求:项目中有一个全局的数据存储者,这个存储者只能有一个,不然会需要进行同步,增加复杂度。
在一个多方工作的情况下如果有使用者不小心又new了这个对象,就会导致数据可能会产生两边的不一致,就需要同步这些数据,无形之中增加了复杂度,这样一个全局储存对象必然要变成单例模式。
假设我们的全局储存对象为store,首先创建store类,然后在内部创建store变量用于存储数据,我们需要保证无论怎么new store类只能返回同一个对象,可以通过store下的install属性来判断,如果有这个属性就直接返回该属性,如果没有就把store.install属性赋值为this,this在使用new操作符的时候就指向store本身。然后我们在store类的外部将install属性初始化为null,代码示例如下:
通过以上代码可以发现,无论怎么new store类拿到的都是同样的对象,例如:
代码示例中创建了两个store,因为它们都指向同一个对象,当修改s1的属性时,s2的属性也改变了
此时如果去掉单例模式验证一下,将store类的代码修改为:
运行结果如下:
此时因为每次new都会创建新的对象,所以s1和s2不指向同一个对象,导致数据要维护两份,单例模式的目的就是减少多个对象的干扰,使我们编程更加简单。
需求:vue-router必须保障全局有且仅有一个,否则会错乱。
vue-router源码里怎样保证全局只有一个的?代码如下:
上面代码就是防止vue-router重复注册的代码,我们在使用vue-router的时候会调用vue.use方法去注册vue-router,其实在调用vue.use的时候就会去执行install方法,所以只要保证install方法每次在调用的时候判断一下是否已经被use过了,如果已经use过了就不执行后面的代码。
它的实现方式也很简单,在外层定义一个_Vue变量,install方法每次都会接收到一个参数Vue,这个参数就是Vue的类,然后内部判断了install方法下面有没有installed属性并且_Vue等于Vue类,就直接return不执行后面的内容。如果没有installed属性就说明没有被赋值过,将installed属性设为true,再将外面定义的_Vue变量赋值为Vue类,这样在下次调用install方法的时候就直接进入判断条件中断执行了。这跟我们前面演示的单例模式非常类似,都是通过方法的一个属性来判断,vue-router中额外加了一个判断也就是外部的_Vue变量是否等于传进来的Vue类,通过多重判断保障代码安全性。