4.1 设计模式概论
4.1.1 代码与设计模式
🍎 我们写代码到底是在写什么?
我们写项目其实就是写模块然后设计它们之间的沟通,设计模式说白了就是帮助我们更好的设计模块, 更好的组织它们之间的沟通。
🍎 设计模式扮演的角色
-
帮助我们组织模块
通过一些设计模式,组织模块间的组成结构
-
帮助我们设计沟通
有的设计模式可以帮助我们设计模块间如何沟通
-
提高代码质量
通过设计模式,让代码更加优雅
🍎 设计原则
-
开闭原则
我们的程序要对扩展开放,对修改关闭;我们的程序要给具体使用的时候扩展的接口,但是在具体使用的时候不能让其修改我们的源码, 也就是说我们不用修改源码就能扩展功能,像vue,react等都有扩展的接口。
-
单一职责原则
我们的模块只做一件事情,模块的职责越单一越好。
-
依赖倒置原则
我们的上层模块不要依赖与具体的下层模块,应该依赖于抽象
假如下面是一套点餐系统,我们在上层和下层之间加一个抽象层;下层如何变动都不会影响到上层,只需更改抽象层即可。
// 具体层
function Food1 (){}
function Food2 (){}
function Food3 (){}
// 抽象层
function resturn (food) {
var list={
food1:new Food1(),
food2:new Food2(),
food3:new Food3(),
}
return list[food]
}
// 上层
function order (food){
return resturn(food)
}
- 接口隔离原则
我们的接口要细化,功能要单一,一个接口不要调用太多方法,使其能力单一,听起来像单一职责原则;但是2者的关注点不同, 单一职责原则主要关注于模块本身,接口隔离原则关注于接口;我们尽量细化接口,每个接口做的事情尽量单一化。
- 迪米特法则
我们让2个对象之间产生沟通,我们最好让2个对象之间知道的越少越好,没必要2者之间非常的了解;我们的中介者模式是一个很好体现迪米特法则的设计模式,中介者模式让2个对象之间没必要直接的沟通,如果直接沟通需要了解2者之间的api和彼此的调用方式,这个时候我们可以采用一个中介者来转达我们的需求,而不用彼此知道
- 里氏替换原则
它主要关注于继承,它的意义是任何使用父类的地方都可以用子类去替换,直白的说我们子类继承父类的时候,我们的子类必须完全保证继承父类的属性和方法,这样的话父类使用的地方,子类可以进行替换
后面学习到设计模式都是在体现这些设计原则
4.1.2 设计模式的分类
- 创建型
这些设计模式可以帮助我们优雅地创建对象
- 结构型
帮助我们优雅地设计代码结构
- 行为型
模块之间行为的模式总结,帮助我们组织模块行为
- 技巧型
一些帮助我们优化代码的技巧
🍎 创建型
-
工厂模式-大量创建对象
-
单例模式-全局只能有我一个
-
建造者模式-精细化组合对象
-
原型模式-javaScript的灵魂
🍎 结构型
-
外观模式-给你的一个套餐
-
适配器模式-用适配代替更改
-
装饰者模式-更优雅地扩展需求
-
享元模式-共享来减少数量
-
桥接模式-独立出来,然后再对接过去
🍎 行为型
-
观察者模式-我作为第三方转发
-
状态模式-用状态代替判断
-
策略模式-算法工厂
-
职责链模式-像生产线一样组织模块
-
命令模式-用命令去解耦
-
迭代器模式-告别for循环
🍎 技巧模式
-
链模式-链式调用
-
委托模式-让别人代替你收快递
-
数据访问模式-一个方便的数据管理器
-
惰性模式-我要搞机器学习(第一次执行完后把状态记录下来)
-
等待者模式-等你们都回来再吃饭
4.2 封装与对象
创建型设计模式到底是怎么样使用的,利用创建型设计模式更好的封装代码更好的创建对象
封装的目的?
-
定义的变量不会污染到外部
-
能够作为一个模块调用
-
遵循开闭原则
什么是好的封装?
-
变量外部不可见
-
调用接口使用
-
留出扩展接口
4.2.1 封装对象时的设计模式
🍎 创建一个对象的模式
-
工厂模式
目的:方便我们大量创建对象
应用场景:当某一个对象需要经常创建的时候
-
建造者模式
目的:需要组合出一个全局对象
应用场景:当要创建单个、庞大的组合对象时
🍎 保障对象全局只有一个
-
单例模式
目的:需要确保全局只有一个对象
应用场景:为了避免重复新建,避免多个对象存在相互干扰
4.2.2 基本结构
🍎 工厂模式的基本结构
function Factory (type) {
switch (type) {
case 'type1'
return new Type1()
case 'type2'
return new Type2()
case 'type3'
return new Type3()
}
}
工厂模式就是写一个方法,只需调用这个方法,就能拿到你要的对象
🍎 建造者模式的基本结构
// 模块1
function Mode1(){
}
// 模块2
function Mode2(){
}
// 最终使用的类
function Final(){
this.mode1=new Model();
this.mode2=new Mode2()
}
把一个复杂的类各个部分,拆分成独立的类,然后再最终类里组合到一块,final为最终给出去的类
🍎 单例模式的基本结构
单例模式的做法不是很固定,我们更重要的记住是保证全局只有一个对象的思想
// 作为单例实例化的对象
let SingLeton=function(name){
this.name=name
}
/*
在SingLeton挂在一个getInstance方法,只能通过getInstance方法来获取
SingLeton的实力化对象
*/
SingLeton.getInstance=function(name){
if(this.instance){
return this.instance
}
return this.instance=new SingLeton(name)
}
通过定义一个方法,使用时只允许通过此方法拿到存在内部的同一实力化对象
4.2.3 应用示例
🍎 工厂模式的示例
实现一个多彩的弹窗?
需求:弹窗有多种,它们之间存在内容和颜色上的差异
(function(ROOT){
// 消息弹性
function infoPop(){
}
// 确认弹窗
function confirmPop(){
}
// 取消弹窗
function cancelPop(){
}
// 弹窗工厂
function pop (type,content,color){
switch(type){
case 'infoPop':
return new infoPop(content,color)
case 'confirmPop':
return new confirmPop(content,color)
case 'confircancelPopmPop':
return new cancelPop(content,color)
}
}
ROOT.pop=pop
})(window)
// 根据传入不同的参数来,来弹出不同的弹窗
pop('infoPop','开始','white')
上面这种写法有个弊端就是不能new pop,当用户用new关键词就不适用了
(function(ROOT){
function pop (type,content,color){
if(this instanceof pop){
var s=this[type](content,color)
return
}else {
return new pop(type,content,color)
}
}
pop.prototype.infoPop= function (){
}
pop.prototype.confirmPop= function (){
}
pop.prototype.cancelPop= function (){
}
ROOT.pop=pop
})(window)
pop('infoPop','开始','white')
jQuery源码示例
需求:jQuery需要操作dom,每个dom都是一个jq对象
/*
seletor 选择器
context 上下文
*/
(function(ROOT){
var jQuery=function(){
//之所以不new jQuery本身,避免递归造成无限循环,我们用的Query.fn上的init方法代替
return new jQuery.fn.init(seletor,context)
}
// JQuery各种操作都会挂载到prototype上
JQuery.fn=JQuery.prototype={
init:function(){
}
}
jQuery.fn.init.prototype=jQuery.fn
// extend方法把方法拷贝extend对象上
jQuery.extend=jQuery.fn.extend=function(){
}
jQuery.extend({
})
ROOT.$=ROOT.jQuery=jQuery
})(window)
总结:工厂模式就是要把我们要暴露的对象,真正要实例化的对象先封装到函数的内部,然后我们只暴露一个工厂方法,使用者通过这个工厂方法 来获取我们实例话的对象,它的优势方便我们大量的创建对象。
🍎 建造者模式的示例
编写一个编辑器插件
需求:有一个编辑器插件,初始化的时候需要配置大量参数,而且内部功能很多
// 最终类
function Editor(){
this.intt=new initHTML()
this.fontControll=new fontControll()
this.stateControll=new stateControll()
}
// 初始化html的类,最终渲染成dom
function initHTML(){}
// 初始化控制样式的方法
initHTML.prototype.initStyle=function(){}
// 初始化渲染成dom的方法
initHTML.prototype.renderDom=function(){}
// 改变字体颜色大小的类
function fontControll(){}
// 控制颜色的方法
fontControll.prototype.changeColor=function(){}
// 控制字体大小的方法
fontControll.prototype.changeFontsize=function(){}
// 改变状态类,为了前进后退
function stateControll(){
this.state=[] //存储状态
this.nowstate=0 //状态指针
}
//保存状态的方法
stateControll.prototype.saveState=function(){}
//回滚状态的方法
stateControll.prototype.saveBack=function(){
var state=this.state[this.nowstate-1]
this.fontControll.changeColor(state.color)
this.fontControll.changeFontsize(state.color)
}
//前进状态的方法
stateControll.prototype.saveGo=function(){}
window.Editor=Editor
建造者模式是把它的模块抽离出独立的类,然后在组合起来
vue初始化
需求:vue内部众多模块,而且过程复杂,vue类也可以看做是一个建造者模式
function Vue(options){
// 只允许用户用new操作符,如果直接调用就抛出警告
if(!(this instanceof Vue)){
console.warn('Vue is a constructor and should be called with the `new` keyword')
}
// 初始化配置项
this._init(options)
}
initMixin(Vue) // 初始化系统混入到Vue中去
stateMixin(Vue) // 状态系统混入到Vue中去
eventsMixin(Vue) // 事件系统的混入
lifecycleMixin(Vue) // 生命周期混入
renderMixin(Vue) // 渲染混入
通过这些方法和我们上一个案例把模块独立成一个类,把这些类放到暴露出的类里面是一个道理; 只不过这里改成方法,vue中所有的功能都是独立开发,通过这一系列的混入将其混入进去
🍎 单例模式的示例
写一个数据存储对象
需求:项目中有一个全局的数据存储者,这个存储者只能有一个 ,不然会需要进行同步,增加复杂度
function store(){
this.state={}
if(store.install){
return store.install
}
store.install=this
}
store.install=null
var s1=new store()
var s2=new store()
s1.state.a=1
console.log(s1,s2) // store { state: { a: 1 } } store { state: { a: 1 } }
vue-router
需求:vue-router必须 保障全局有且只有一个,否则的话会错乱
let _Vue
function install (Vue){
if(install.installed && _Vue===vue) return
install.installed=true
_Vue=vue
}
4.2.4 总结
工厂模式:如果你写的模块,需要大量创建类似的对象
建造者模式:需要创建一个需要大量参数,且内部模块庞大
单例模式:防止重复注册,防止有多个对象互相干扰
4.3 提高复用性
什么是好的复用?
- 对象可以再重复使用,不用修改
- 重复代码少
- 模块功能单一
4.3.1 提高复用性的设计模式
🍎 减少代码数量,高效复用代码
-
桥接模式
目的:通过桥接代替耦合
应用场景:减少模块之间的耦合
-
享元模式
目的:减少对象/代码数量
应用场景:当代码中创建了大量类似对象和类似的代码块
🍎 创建高可复用性的代码
-
模版方法模式
目的:定义一系列操作的骨架,简化后面类似操作的内容
应用场景:当项目中出现很多类似操作内容
4.3.2 基本结构
🍎 桥接模式
// 有三种形状,每种形状都有3种颜色
function rect (color){ //矩形
showcolor(color)
}
function circle () { // 圆形
showcolor(clor)
}
function delta (color){ // 三角形
showcolor(clor)
}
new circle("red")
- 对于3种形状、每种形状有3种颜色的需求,可以不用创建9种不同颜色的不同形状
- 这个模式把重复的方法抽取出来,然后在桥接出去
这个模式跟我们的建造者模式很类似拆分再组合,建造者模式的核心是如何去构造对象;而我们桥接模式是如何简化我们的代码,提高我们的可复用性,一个关注的是功能一个关注的是创建,这是它们的区别。
🍎 享元模式
// 有一百种不同文字的弹窗,每种弹窗行为相同,但是文字和样式不同,我们没必要新间一百个弹窗对象
function Pop(){
}
// 保留同样的行为
Pop.prototype.action=function(){}
//显示
Pop.prototype.show=function(){}
// 提取出每个弹窗不同的部分作为一个外部数组
var popArr=[
{text:"window1",style:[400,400]}
{text:"window2",style:[400,200]}
]
var poper=new Pop()
for(var i=0;i<100;i++){
poper.show(popArr[i])
}
- 只需一个类,不需要new一百次弹窗
- 这个类只保留所有弹窗共有的,每个弹窗不同的部分留作为一个公共享元
🍎 模版方法模式
// 编写导航组件,有的带消息提示,有的竖着,有的横者
function baseNav(){
// 基础类,此处定下基本骨架
}
baseNav.prototype.action=function(fn){
// 特异性的处理,留一个回调等待具体实现
}
- 导航组件多种多样,可能后面还会新增类型,那么我们不妨写一个基础的组件库,然后具体的实现,延迟到具体的使用时
4.3.3 应用示例
🍎 桥接模式的示例
创建不同的选中效果
需求:有一组菜单,上面每种选项都有不同的选中效果
// 一般做法
//menu1,menu2,menu3
function menuItem(word){
this.word="";
this.dom=document.createElement('div');
this.dom.innerHTML=this.word;
}
var menu1=new menuItem('menu1');
var menu2=new menuItem('menu2');
var menu3=new menuItem('menu3');
menu1.onmouseover=function(){
menu1.style.color='red';
}
menu2.onmouseover=function(){
menu1.style.color='green';
}
menu3.onmouseover=function(){
menu1.style.color='blue';
}
menu1.onmouseout=function(){
menu1.style.color='white';
}
menu2.onmouseout=function(){
menu1.style.color='white';
}
menu3.onmouseout=function(){
menu1.style.color='white';
}
// 桥接模式做法
function menuItem(word,color){
this.word=word;
this.color=color;
this.dom=document.createElement('div');
this.dom.innerHTML=this.word;
document.getElementById('app').appendChild(this.dom);
}
menuItem.prototype.bind=function(){
var self=this;
this.dom.onmouseover=function(){
console.log(self.color);
this.style.color=self.color.colorOver;
}
this.dom.onmouseout=function(){
this.style.color=self.color.colorOut;
}
}
function menuColor(colorover,colorout){
this.colorOver=colorover;
this.colorOut=colorout;
}
var data=[{word:'menu1',color:['red','black']},{word:'menu2',color:['green','black']},{word:'menu3',color:['blue','black']}]
for(var i=0;i<data.length;i++){
new menuItem(data[i].word,new menuColor(data[i].color[0],data[i].color[1])).bind();
}
Express中创建get等方法
需求:express中get,post等方法,有七八个,如何快速地创建。
var methods=['get','post','delete','put'];
methods.forEach(function(method){
app[method]=function(){
//利用桥接过来的route来完成post,get等请求
route[method].apply(route,slice.call(arguments,1))
}
})
🍎 享元模式的示例
文件上传
需求:项目中有一个文件上传功能,该功能可以上传多个文件
一般做法
//文件上传
function uploader(fileType,file){
this.fileType=fileType;
this.file=file;
}
uploader.prototype.init=function(){
//初始化文件上传的html
}
uploader.prototype.delete=function(){
//删除掉该html
}
uploader.prototype.uploading=function(){
//上传
}
new uploader('img',fileob1)
new uploader('txt',fileob2)
new uploader('img',fileob3)
new uploader('word',fileob4)
享元模式
//fileType,file
function uploader(){
}
uploader.prototype.init=function(){
//初始化文件上传的html
}
uploader.prototype.delete=function(){
//删除掉该html
}
uploader.prototype.uploading=function(filetype,file){
}
var fileob1,fileob2,fileob3,fileob4
var data=[
{
type:'img',
file:fileob1
},
{
type:'txt',
file:fileob2
},
{
type:'img',
file:fileob3
},
{
type:'word',
file:fileob4
},
]
var uploader=new uploader();
for(var i=0;i<data.length;i++){
uploader.uploading(data[i].type,data[i].file);
}
jQuery的extend
需求:extends方法,需要判断参数数量来进行不同的操作
// 一般做法
var jQuery={};
jQuery.fn={};
jQuery.extend = jQuery.fn.extend = function() {
if(arguments.length==1){
for(var item in arguments[0]){
this[item]=arguments[0][item]
}
}else if(arguments.length==2){
for(var item in arguments[1]){
arguments[0][item]=arguments[1][item]
}
return arguments[0];
}
}
// 享元做法,保留一个公共的for循环
jQuery.extend = jQuery.fn.extend = function() {
var target=arguments[0];
var source;
if(arguments.length==1){
target=this;
source=arguments[0];
}else if(arguments.length==2){
target=arguments[0];
source=arguments[1];
}
for(var item in source){
target[item]=source[item]
}
return target;
}
🍎 模版方法模式的示例
编写一个弹窗组件
需求:项目有一系列弹窗,每个弹窗的行为、大小、文字不同
function basePop(word,size){
this.word=word;
this.size=size;
this.dom=null;
}
basePop.prototype.init=function(){
var div=document.createElement('div');
div.innerHTML=this.word;
div.style.width=this.size.width+'px';
div.style.height=this.size.height+'px';
this.dom=div;
}
basePop.prototype.hidden=function(){
//定义基础操作
this.dom.style.display='none';
}
basePop.prototype.confirm=function(){
//定义基础操作
this.dom.style.display='none';
}
function ajaxPop(word,size){
basePop.call(this,word,size);
}
ajaxPop.prototype=new basePop();
var hidden=ajaxPop.prototype.hidden;
ajaxPop.prototype.hidden=function(){
hidden.call(this);
console.log(1);
}
var confirm=ajaxPop.prototype.confirm;
ajaxPop.prototype.confirm=function(){
confirm.call(this);
console.log(1);
}
var pop=new ajaxPop('sendmes',{width:100,height:300});
pop.init();
pop.confirm();
var axios={get:function(){
return Promise.resolve();
}};
上面这个就是我们面向对象中的继承,模版模式不一定非要通过继承方式来完成,它强调先定义后面进行不同维度的操作的基本行为;
然后在这个基本行为上有扩展的空间,这就是我们模版方法的目的。
封装一个算法计算器
需求:现在我们有一系列自己的算法,但是这个算法常在不同的地方需要增加一些不同的操作
//算法计算器
function counter(){
this.beforeCounter=[];
this.afterCounter=[];
}
//然后我们把具体的不同部分留到具体使用的时候去扩展
//所以我们定义两个方法来扩展
counter.prototype.addBefore=function(fn){
this.beforeCounter.push(fn);
}
counter.prototype.addAfter=function(fn){
this.afterCounter.push(fn);
}
//最终计算方法
counter.prototype.count=function(num){
//结果变量
var _resultnum=num;
//算法队列数组组装
var _arr=[baseCount];
_arr=this.beforeCounter.concat(_arr);
_arr=_arr.concat(this.afterCounter);
//不同部分的相同算法骨架
function baseCount(num){
num+=4;
num*=2;
return num;
}
//循环执行算法队列
while(_arr.length>0){
_resultnum=_arr.shift()(_resultnum);
}
return _resultnum;
}
//使用
var countObject=new counter();
countObject.addBefore(function(num){
num--;
return num;
})
countObject.addAfter(function(num){
num*=2;
return num;
})
countObject.count(10);
这个应用了组合实现模版模式
javascript的组合与继承
-
组合(推荐)
- javascript最初没有专门的继承,所以最初javascript推崇函数式的编程,然后进行统一组合桥接到一起
- 桥接模式可以看出组合的一种体现,组合的好处是耦合低,方便方法复用,方便扩展
-
继承
- es6出现class与extend,继承的方式多种多样,但是都是各有弊端
- 模版方法模式可以看出继承的一种体现,继承的好处是可以自动获得父类的内容与接口,方便统一化
4.3.4 总结
- 桥接模式
通过独立方法间接的桥接来形成整体功能,这样每个方法都可以被高度复用
- 享元模式
提取出公有部分与私有部分,私有部分作为外部数据传入,从而减少对象数量
- 模版方法模式
当一个功能朝着多样化发展,不妨先定义一个基础的,把具体实现延迟到后面
4.4 提高可扩展性(1)
提高可扩展性的目的
- 面对需求变更,方便更该需求
- 减少代码修改的难度
什么是好的扩展
- 需求的变更,不需要重写
- 代码修改不会引起大规模变动
- 方便加入新模块
4.4.1 提高可扩展性的设计模式
🍎 更好的更改代码
- 适配器模式(接口)
适配器模式的目的:通过写一个适配器,来代替替换
适配器模式的应用场景:面临接口不通用的问题
- 装饰者模式(方法作用)
装饰者模式的目的:不重写方法的扩展方法
装饰者模式的应用场景:放一个方法需要扩展,但是又不好去修改方法
🍎 解耦你得方法与调用
- 命令模式
命令模式的目的:解耦实现和调用,让双方互不干扰
命令模式的应用场景:调用的命令充满不确性
4.4.2 基本结构
🍎 适配器模式的基本结构
var log=(function(){
return winodw.console.log
})
想用log来代替console.log
🍎 装饰者模式的基本结构
// 在一个他人写好的模版a内部调用b,模块为他人写好,不能修改,如何扩展b方法?
var a={
b:function(){}
}
function myb(){
a.b()
// 要扩展的方法
}
我们新建一个自己的方法,在其内部调用b方法,并且再执行自己的方法,这样可以在不修改原方法的情况下扩展方法
🍎 命令模式的基本结构
var command=(function(){
// action中定义了各种方法
var action={}
// excure可以调用action方法
return function excure()
})()
// command只需输入命令就可以调用action里的方法
4.4.3 应用示例
🍎 适配器模式示例
框架的变更
需求:目前项目使用的A框架,现在改成了B,2个框架与十分类似,但是有少数几个方法不同
// A框架调用的方式
A.c()
// 假如我们项目中换成了jQuey,我们不想全部去替换A方法,就用适配器的方法
A.c=function(){
return $.on.apply(this.arguments)
}
参数适配
需求:为了避免参数不适配产生问题,很多框架会有一个参数适配操作
// 给参数适配,没传值给默认值
function f1(){
var _defalut={
name:"",
color:""
}
for(var item in _defalut){
_defalut[item]=config[item] || _defalut[item]
}
return _defalut
}
🍎 装饰者模式示例
扩展你的已有事件绑定
需求:现在项目改造,需要给input标签已经有的事件增加一些操作
var decorator=function(dom,fn){
if(typeof dom.onclick='function'){
var _old=dom.onclick;
dom.onclick=function(){
_old();
fn();
}
}
}
decorator(document.getElementById('dom1'),function(){
// 自己的操作
})
Vue的数组监听
需求:vue中利用defineProperty可以监听对象,那么数组怎么办
var arrayProto=Array.prototype
var arrayMethods=Object.create(arrayProto)
var methodsToPatch=[
'push',
'pop',
'unshift',
'shift',
'splice',
'resverse',
'sort'
]
methodsToPatch.forEach(method=>{
var original=arrayMethods[method]
object.defineProperty(arrayMethods,method,{
value(...args){
const result =original.apply(this,args)
dep.notify()
return result
}
})
})
装饰者模式,拿到老方法,调用老方法,组成新方法
🍎 命令模式示例
需求:封装一系列的canvas绘图命令
var myCanvas=function(){
}
myCanvas.prototype.drawCircle=function(){
}
myCanvas.prototype.drawRect=function(){
}
var canvasComand=(function(){
var action={
drawCircle:function(config){
},
drawRect:function(config){
}
};
return function excute(commander){
commander.forEach((item)=>{
action[item.command](item.config);
})
}
})()
myCanvas([{command:'drawReact',config:{}}])
-
用户只管输入他要的命令,不用关心api
-
命令和实现解耦,无论命令发生变动,还是实现发生变动,都不会彼此影响
绘制随数量图片
需求:需要做一个画廊,图片数量和排列顺序随机
var createImg=(function(){
var action={
create:function(obj){
var htmlArr=[];
var _htmlstring='';
var _htmlTemplate="<div><img src='{{img-url}}' /></div><h2>{{title}}</h2>"
var displayWay={
normal:function(arr){
return arr;
},
reverse:function(arr){
return arr.reverse;
}
}
obj.imgArr.forEach((img)=>{
var _html;
_html=_htmlTemplate.replace('{{img-url}}',img.img).replace('{{title}}',img.title);
htmlArr.push(_html);
})
htmlArr=displayWay[obj.type](htmlArr);
_htmlstring=htmlArr.join("");
return "<div>"+_htmlstring+"</div>";
},
display:function(obj){
var _html=this.create(obj);
obj.target.innerHTML=_html;
}
}
return function excute(obj){
var _default={
imgArr:[{img:'xxxx',title:'default title'}],
type:'normal',
target:document.body
};
for(var item in _default){
_default[item]=obj[item]||_default[item];
}
action.display(_default);
}
})()
createImg({
imgArr:[{img:'xxxx',title:'default title1'},{img:'xxxx',title:'default title2'}],
type:'normal',
})
数据-> excute命令解析层 -> 调用api
4.4.4 总结
- 适配器模式
当面临两个新老模块间接口api不匹配,可以用适配来转化api
- 装饰者模式
当老的方法,不方便去直接修改,可以通装饰者来增加功能
- 命令模式
解耦实现与具体命令,让实现端和命令端扩展的都更轻松
4.5 提高可扩展性(2)
提高整体项目可扩展性的核心
-
低耦合
-
良好的组织沟通方式
4.5.1 提高可扩展性的设计模式
🍎 应对需求上的变更
- 观察者模式(事件绑定是典型的观察者模式,比如dom上监视点击了事件,点击事件触发以后就去做这个点击事件)
观察者模式的目的:减少对象间的耦合,来提高可扩展性
观察者模式的应用场景:当两个模块直接沟通会增加它们的耦合性时
- 职责链模式
职责链模式的目的:为了避免请求发送者与多个请求处理者耦合在一起,形成一个链条
组合模式的应用场景:把操作分隔成一系列模块,每个模块只处理自己的事情
🍎 应对需求上的变更
访问者模式的目的:解耦数据结构与数据操作
访问者模式的应用场景:数据结构不希望与操作有关联
4.5.2 基本结构
🍎 观察者的基本结构
function observe {
this.message={}
}
observe.prototype.regist=function(type,fn) {
this.message[type]=fn
}
observe.prototype.fire=function(type){
this.message[type]()
}
observe.prototype.remove=function(type){
this.message[type]=null
}
- 定义一个中转观察者,两个模块之间不直接沟通,而是通过观察者,一般使用与不方便直接沟通,或者异步操作
🍎 职责链模式的基本结构
function mode1 () {
}
function mode2 () {
}
function mode3 () {
}
_result=mode1(_result)
_result=mode2(_result)
_result=mode3(_result)
- 把要做的事情组织为一条有序的链条,通过再这条链条传递消息来完成功能,适用于不设计到赋值异步操作
🍎 访问者模式的基本结构
var data=[]
var handler=function () {
}
handler.prototype.get=function(){
}
var vistor=function (handler,data){
handler.get(data)
}
- 通过定义一个访问者,代替直接访问对象,来减少两个对象之间的耦合
4.5.3 应用示例
🍎 观察者模式示例
多人合作的问题
需求:现在假设A工程师写了首页模块,然后B工程师写了评论模块。现在要把评论展示在首页
function observe {
this.message={}
}
observe.prototype.regist=function(type,fn) {
this.message[type]=fn
}
observe.prototype.fire=function(type){
this.message[type]()
}
function comment () {
var self=this;
this.commentList=[
{
type:'hot',
content:'xxxx'
}
];
// 注册事件
observeOb.regist('gotHot',function(){
var _arr=[];
self.commentList.forEach((item)=>{
if(item.type==='hot'){
_arr.push(item)
}
})
return _arr
})
}
// 调用事件
var _arr=observeOb.fire('gotHot')
一个转盘
需求:有一个转盘应用,每转一圈,速度加快
function observe {
this.message={}
}
observe.prototype.regist=function(type,fn) {
this.message[type]=fn
}
observe.prototype.fire=function(type){
this.message[type]()
}
var observeOb=new observe()
// 初始化html=> 最终结果选定 => 运动结果 => 运动控制
var _domArr=[]
function htmlInit (target) {
for(let i =0;i<9;i++){
var _div=document.createElement('div')
_div.innerHTML=i
_div.setAttribute('class','item')
target.appendChild(_div)
_domArr.push(_div)
}
}
function getFinal(){
var _num=Math.random()*10+40
return Math.floor(_num,0)
}
// 运动模块
function mover (moveConfig){
var nowIn=0;
var removeNum=9;
var timer=setInterval(()=>{
if(nowIn!=0){
removeNum=nowIn-1
}
_domArr[removeNum].setAttribute('class','item')
_domArr[nowIn].setAttribute('class','item item-on')
nowIn++
if(nowIn==moveConfig.moveTime){
clearInterval(timer)
if(moveConfig.moveTime==10){
observeOb.fire('finish')
}
}
},moveConfig.speed)
}
function moveControll () {
var final=getFinal()
var _circle=Math.floor(final/10,0)
var stopNum=final%10
var _speed=2000
var _runCircle=0
mover({
moveTime:10,
speed:_speed
})
observeOb.regist('finish',fucntion(){
var _time=0;
_speed-=50;
_runCircle++;
if(_runCircle<=_circle){
_time=0
}else {
_time=stopNum
}
mover({
moveTime:_time,
speed:_speed
})
})
}
htmlInit(document.getElementById('app'))
moveControll()
🍎 职责链模式示例
Axios的拦截器
需求:axios拦截器的设计,大家可以看成一个用给职责链的思想去处理请求
function axios(){
this.interceptors={
request:new interceptorsManner(),
response: new interceptorsManner()
}
axios.prototype.request=function (){
var chain=[dispathReuest,undefined]
var promise=Promise.resolve(config)
this.interceptors.request.handlers.forEach((interceptor)=>{
chain.unshift(interceptor.fulfilled,interceptor.injected)
})
this.interceptors.response.handlers.forEach((interceptor)=>{
chain.shift(interceptor.fulfilled,interceptor.injected)
})
while(chain.length){
promise=promise.then(chain.shift(),chain.shift())
}
}
}
function interceptorsManner (){
this.handlers=[]
}
interceptorsManner.prototype.use=function(fulfilled,rejected){
this.handlers.push({
fulfilled:fulfilled,
rejected:rejected
})
}
利用职责链组织一个表单验证
需求:有一个表单,需要前后台校验,后台校验
// 表单事件绑定->表单前端验证->表单后端验证
// 思想:把你要做的事情拆分为模块,模块之间只做自己模块的事情
input.onblur=function() {
var _value=input.value
var _arr=[font,middle,back,fontAgain]
async function test(){
var _result=_value
while (_arr.length>0){
_result=await _arr.shift()(_result)
}
return _result
}
test().then((res)=>{
console.log(res)
})
}
function font (result){}
function middle (result){}
function back (result){}
function fontAgain (result){}
🍎 访问者模式示例
不同角色访问数据
需求:假设有一个公司的财务报表,财务关心支出和收入,老板关心盈利
function report () {
this.income=""
this.cost=""
this.profit=""
}
function boss () {}
boss.prototype.get=function(data) {}
function account () {}
account.prototype.get=function (num1,num2){}
function vistor (data,man) {
var handle={
boss:function (data){
man.get(data.profit)
},
account:function (data){
man.get(data.income,data.cost)
}
}
handle[man.constructor.name](data)
}
vistor(new report(),new account())
vistor(new report(),new boss())
// 设计的数据结构操作难以去访问具体的数据结构的时候
表格操作
需求:一个可以新增,删除的表格
function table () { }
table.prototype.show=function () {
}
table.prototype.delete=function () {
vistor(this.tableData,'delete',id)
}
table.prototype.add=function () {
}
var tableData=[
{
id:1,
name:'xxx',
prize:'xxx'
}
]
function vistor (table,data,handle) {
var handleOb={
delete:function(id){
},
add:funtion(id,name,price){
}
}
var arg=Array.prototype.splice(arguments);
arg.splice(0,3);
handleOb[handle].apple(this,arg)
}
4.5.4 总结
- 观察者模式
适用于不适合直接沟通的模块之间的组织
- 职责链模式
组织同步模块,把要做的事情划分为模块,要做的事情一次传递
- 访问者模式
解耦数据操作与数据结构
4.6 提高代码质量
提高代码质量
- 高质量的代码,方便后续的一切操作
- 方便他人阅读
什么是代码质量
- 代码整洁
- 结构规整,没有漫长的结构
- 阅读好理解
4.6.1 优化代码结构
- 策略模式/状态模式
策略/状态模式的目的:优化if-else分支
策略/状态模式的应用场景:当代码if-else分支过多时
- 外观模式
外观模式的目的:通过为多个复杂的子系统提供一个一致的接口
外观模式的应用场景:当完成一个操作时,需要操作多个子系统,不如提供一个更高级的
4.6.2 优化你的代码操作
- 迭代器模式
迭代器者模式的目的:不访问内部的情况下,方便的遍历数据
迭代器模式的应用场景:当我们需要对某个对象进行操作,但是又不能暴露内部
- 备忘录模式
备忘录模式的目的:记录状态,方便回滚
备忘录模式的应用场景:系统状态多样,为了保证状态的回滚方便,记录状态
4.6.3 基本结构
🍎 策略模式的基本结构
假设要编写一个计算器,有加减乘除,我们把一层一层的if判断,变成下面的形式
function Strategy (type,a,b) {
var Strategyer={
add:function (a,b){
return a+b
}
minus:function(a,b){
return a-b
}
division:function (a,b){
return a/b
}
}
return Strategyer[type](a,b)
}
🍎 状态模式的基本结构(加了状态的策略模式)
为了减少if-else结构,将判断变成对象内部的一个状态,通过对象内部的状态改变,让其拥有不同的行为
function stateFactor (state) {
var stateObject={
_status:'',
state:{
state1:function () {
},
state2:function (){
}
},
run:function () {
return this.state[this._status]
}
}
stateObject._status=state
return stateObject
}
🍎 外观模式的基本结构
我们组织方法模块时可以细化多个接口,但是我们给别人使用时,要合为一个接口就像你可以直接去餐厅去点套餐
// 模块1
function Model1 () {
}
// 模块2
function Model2 () {
}
// 功能由Model1获取Model2得结果来完成
function use () {
Model2(Model1())
}
🍎 迭代器模式的基本结构
在不暴露对象内部结构的同时,可以顺序的访问对象内部,可以帮助我们简化循环,简化数据操作
function Iterator (item){
this.item=item
}
Iterator.prototype.dealEach=function (fn) {
for(var i=0;i<this.item.length;i++){
fn(this.item[i],i)
}
}
🍎 备忘录模式的基本结构
记录对象内部的状态,当有需要时回滚到之前的状态或者方便对象使用
function Memento () {
var cache={}
return function (cacheName){
if(cache[cacheName]){
// 有缓存的操作
}else {
// 没有缓存的操作
}
}
}
var MementtoFn=Memento()
MementtoFn('xxxcx')
4.6.4 应用示例
🍎 策略/状态模式的示例
动态的内容
需求:项目有一个动态的内容,根据用户权限的不同显示不同的内容
// 没有用策略的模式的情况
function showPart1(){
console.log(1);
}
function showPart2(){
console.log(2);
}
function showPart3(){
console.log(3);
}
axios.get('xxx').then((res)=>{
if(res=='boss'){
showPart1();
showPart2();
showPart3();
}else if(res=='manner'){
showPart1();
showPart2();
}else if(res=='staff'){
showPart3();
}
})
// 用策略模式的情况
function showControl(){
this.status='';
this.power={
boss:function(){
showPart1();
showPart2();
showPart3();
},
manner:function(){
showPart1();
showPart2();
},
staff:function(){
showPart3();
}
}
}
showControl.prototype.show=function(){
var self=this;
axios.get('xxx').then((res)=>{
self.status=res;
self.power[self.status]();
})
}
new showControl().show();
复合运动
需求:有一个小球,可以控制它左右移动,或则左前,右前等方式移动
function moveLeft(){
console.log('left')
}
function moveRight(){
console.log('RigmoveRight')
}
function moveTop(){
console.log('Top')
}
function moveBottom(){
console.log('bomoveBottom')
}
// 没有用状态模式的情况
function mover(){
if(arguments.length==1){
if(arguments[0]=='left'){
moveLeft();
}else if(arguments[0]=='right'){
moveRight();
}else if(arguments[0]=='top'){
moveTop();
}else if(arguments[0]=='bottom'){
moveBottom();
}
}else{
if(arguments[0]=='left'&&arguments[1]=='top'){
moveLeft();
moveTop();
}else if(arguments[0]=='right'&&arguments[1]=='bottom'){
moveRight();
moveBottom();
}
}
}
// 用状态模式的情况
function mover(){
this.status=[];
this.actionHandle={
left:moveLeft,
right:moveRight,
top:moveTop,
bottom:moveBottom
}
}
mover.prototype.run=function(){
this.status=Array.prototype.slice.call(arguments);
this.status.forEach((action)=>{
this.actionHandle[action]();
})
}
new mover().run('left','right');
🍎 外观模式的示例
插件封装的规律
需求:插件基本都会给最终使用提供一个高级接口
// 划分功能,给使用者一个统一的接口
function tab(){
this.dom=null
}
tab.prototype.initHTML=function(){
}
tab.prototype.changeTab=function(){
}
tab.prototype.eventBind=function(){
var self=this;
this.dom.onclick=function(){
self.changeTab();
}
}
tab.prototype.init=function(config){
this.initHTML(config);
this.eventBind();
}
封装成方法的思想
需求:在兼容时代,我们会常常需要检测能力,不妨作为一个统一接口
//dom支持检测
function addEvent(dom,type,fn){
if(dom.addEventListener){
dom.addEventListener(type,fn,false);
}else if(dom.attachEvent){
dom.attachEvent('on'+type,fn)
}else{
dom['on'+type]=fn
}
}
🍎 迭代器模式的示例
构建一个自己的forEach
需求:forEach方法其实是一个典型的迭代器方法
// 对数组和对象进行迭代
//forEach
function Iterator(data){
this.data=data;
}
Iterator.prototype.dealEach=function(fn){
if(this.data instanceof Array){
for(var i=0;i<this.data.length;i++){
fn(this.data[i],i)
}
}else{
for(var item in this.data){
fn(this.data[item],item)
}
}
}
给你的项目数据添加迭代器
需求:项目经常对于后端数据进行遍历操作,不如封装一个迭代器,遍历的更方便
//数据迭代器
var data=[{num:1},{num:2},{num:3}]
function i(data){
function Iterator(data){
this.data=data;
}
Iterator.prototype.getHasSomenum=function(handler,num){
var _arr=[];
var handleFn;
if(typeof handler=='function'){
handleFn=handler;
}else{
handleFn=function(item){
if(item[handler]==num){
return item;
}
}
}
for(var i=0;i<this.data.length;i++){
var _result=handleFn.call(this,this.data[i])
if(_result){
_arr.push(_result);
}
}
return _arr;
}
return new Iterator(data);
}
i(data).getHasSomenum('num',1);
// 自定义的筛选方法
i(data).getHasSomenum(function(item){
if(item.num-1==2){
return item;
}
});
🍎 备忘录模式的示例
文章页的缓存
需求:项目有一个文章页需求,现在进行优化,如果上一篇已经读取过了,则不进行请求,否则请求文章数据
//缓存
function pager(){
var cache={};
return function(pageName){
if(cache[pageName]){
return cache[pageName];
}else{
axios.get(pageName).then((res)=>{
cache[pageName]=res;
})
}
}
}
var getpage=pager();
getpage('pageone');
前进后退功能
需求:开发一个可移动的div,拥有前进后退功能能回滚到之前的位置
//前进后退
function moveDiv(){
this.stateList=[]; // 缓存之前的状态
this.nowState=0; // 状态指针,指向当前状态在哪个
}
/**
* @param {string} type 往哪个方向移动
* @param {Number} num 移动多远
*/
moveDiv.prototype.move=function(type,num){
changeDiv(type,num); //假设移动位置的函数
this.stateList.push({
type:type,
num:num
}); // 添加状态
this.nowState=this.stateList.length-1; // 设置当前指针
}
moveDiv.prototype.go=function(){ //前进
var _state;
if(this.nowState<this.stateList.length-1){ //当前指针小于数组最后一位,说明能前进
this.nowState++;
_state=this.stateList[this.nowState];
changeDiv(_state.type,_state.num);
}
}
moveDiv.prototype.back=function(){ //后退
var _state;
if(this.nowState>=0){
this.nowState--;
_state=this.stateList[this.nowState];
changeDiv(_state.type,_state.num);
}
}
4.6.5 本章总结
- 策略/状态模式
帮助我们优化if-else结构
- 外观模式
一种套餐化接口的思想,提醒我们封装常用方法
- 迭代器模式
帮助我们更好的遍历数据
- 备忘录模式
帮我们缓存以及回到过去的状态
4.7 总结
我们的设计模式,要记住其思想,不用记住其结构,结构不是固定;我们通过设计模式主要是提高我们代码的质量