设计模式一直都是程序员进阶绕不开的话题,有人将其奉为圣经,有人认为设计模式大多数都是形而上学的死板理论。Erich Gamma、Richard Helm 、Ralph Johnson、John Vlisside所组成的四人帮(GOF: Gang of Four)总结了面对对象语言的23种设计模式,将其写在《设计模式:可复用面向对象软件的基础》一书中,《设计模式》最初讲的是静态类型语言中的设计模式,大部分代码由C++写成,对于JavaScrip这种动态语言而言并不是全部适用,甚至很多模式内含在JavaScript语言内部,因此对于JavaScript的设计模式不能对已有模式生搬硬套,而是需要根据其内涵思想进行灵活用运。
var Iterator = function( obj ){
var current = 0;
var next = function(){
current += 1;
};
var isDone = function(){
return current >= obj.length;
};
var getCurrItem = function(){
return obj[ current ];
};
return {
next: next,
isDone: isDone,
getCurrItem: getCurrItem
length: obj.length
}
};
比如如果我们采用内迭代器模式比较两个数组是否相同,可能会是如下:
var compare = function( ary1, ary2 ){
if ( ary1.length !== ary2.length ){
return false;
}
ary1.forEach(, function( i, n ){
if ( n !== ary2[ i ] ){
return false;
}
});
return true;
};
什么是设计模式
设计模式一直都是程序员进阶绕不开的话题,有人将其奉为圣经,有人认为设计模式大多数都是形而上学的死板理论。Erich Gamma、Richard Helm 、Ralph Johnson、John Vlisside所组成的四人帮(GOF: Gang of Four)总结了面对对象语言的23种设计模式,将其写在《设计模式:可复用面向对象软件的基础》一书中,《设计模式》最初讲的是静态类型语言中的设计模式,大部分代码由C++写成,对于JavaScrip这种动态语言而言并不是全部适用,甚至很多模式内含在JavaScript语言内部,因此对于JavaScript的设计模式不能对已有模式生搬硬套,而是需要根据其内涵思想进行灵活用运。
设计模式简单的讲就是针对特定问题的一种解决方案,这些设计模式并不是GOF的发明,而是早已长期存在于软件开发中,GOF所做的就将是这些问题抽象出来并给与恰当的命名。设计模式并不是能够解决任何问题的银弹,而是针对特定问题的解释方案,不仅如此部分设计模式可能还会带来代码量的增加,并且把系统的逻辑搞得更加复杂,因此对于设计模式而言,没有好与坏,只有是否适合于当前你的场景。
GOF所提出的二十三种设计模式可以分为三类:
其中创建范例是指关于如何创建范例的方式。而结构范例是指类与对象的复合关系。行为范例是指对象间如何联系和通讯的。本篇文章首先介绍几种常见的行为范例。
模板方法模式(Template Method Pattern)
模板方法模式是一种利用继承实现的非常简单的设计模式。模板方法模式由两部分构成,一部分是抽象父类,一部分是具体实现的子类。父类用来实现算法级的架构和子类方法的实现顺序,而子类用来实现具体的步骤逻辑。这样子类就能在不改变算法架构的情况下,重新定义算法中的某些步骤。
举一个最经典的咖啡与茶的例子,假设我们泡一杯咖啡的步骤是:
而泡一壶茶的步骤是:
我们可以发现,泡一杯咖啡喝泡一杯茶的过程是大同小异的,经过抽象我们可以整理为以下四个步骤:
因此从代码的角度而言,具体的步骤实现的逻辑可能有所不同,但实际的执行顺序确实相同的。因此我们可以在父类中提取出对应的模板方法:
模板方式模式在前端开发中,尤其是框架层面的设计中非常常见,例如React中,各个组件生命周期函数执行顺序总是不变的,是由React内部封装了模板方法,而各个组件的生命周期函数内部实现细节是不相同的。不仅如此,在模板方法模式中,某些方法足够特殊,可能需要跳过执行某些方法,这个时候我们就可以采用钩子函数(hook),是否需要挂钩,这是由子类自行决定的,钩子函数的返回决定了模板方法后面部分的执行步骤。例如,React就提供了shouldComponentUpdate这个方法,让子类自行决定是否需要执行接下来重新渲染的步骤。
模板方法模式能很好的将变化的逻辑封装在子类,而将不变的逻辑抽象到父类,我们通过增加新的子类,就能不断的为系统扩展新的功能,符合开发-闭合原则。
迭代器模式(Iterator Pattern)
迭代器模式指的是提供一种方法顺序访问聚合对象中的元素并且不暴露对象内部的表示。大部分语言都内置了迭代器实现,JavaScript从ES5就对数组提供了forEach的迭代器。
迭代器分为内部迭代器和外部迭代器,内部迭代器是指内部已经定义好迭代规则,完全接手整个迭代调用。例如上面提到的forEach函数。
外部迭代器必须显式地请求迭代下一个元素,外部迭代器增加了调用的复杂度但增加了迭代器的灵活性。
比如如果我们采用内迭代器模式比较两个数组是否相同,可能会是如下:
上面的比较因为借助了闭包,才能够实现比较的逻辑。对于其他的语言实现起来就相当的麻烦,但是如果采用外迭代器的话,实现起来就会相对灵活:
观察者模式(Observer Pattern)
观察者模式又称为发布-订阅者模式,用于定义对象间一对多的关系,当发布者改变时,所有的订阅者都会得到通知。观察者模式在前端开发中使用十分广泛,最常见的就是事件模型
上面的代码就是最简单的DOM事件绑定,我们并不知道用户会在什么时候点击页面。我们只要订阅document.body上的click事件,当body节点被点击时,就会发布消息,也就是回调函数被执行。观察者模式可以将对象间互相显式调用接口的硬编码解耦,对象间不需要互相了解彼此细节。在MVC和MVVM中使用非常的广泛。
上面是一个最简单的观察者模式,因为JavaScript是采用异步编程,并且函数作为一等公民,实现起来非常的简单。但是在传统的面对对象语言中,实现起来就稍微的麻烦一点。在Vue的Observer模块就实现了一个Emitter,用来处理事件模型:
观察者模式可以很好的实现对象之间的解耦,但是另一方面观察者模式又存在者非常明显的缺点,首先观察者模式会消耗时间和内存,并且如果过度使用的话,对象间的关联都会被掩盖,使得程序难以调试和理解,因为你可能很追溯到事件的起源。
职责链模式
职责链模式是指将请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。
假设我们负责一个售卖手机的电商网站,经过分别交纳500元定金和200元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过500元定金的用户会收到100元的商城优惠券,200元定金的用户可以收到50元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。
如果将这个流程写成代码的话:
上面的代码庞大且难以阅读,并且扩展性差,当我们想要增加另一种购买模式时,只能够深入函数内部修改,这也是违反开闭原则的。当时当我们用职责链改写上面的代码时:
上面的代码庞大且难以阅读,并且扩展性差,当我们想要增加另一种购买模式时,只能够深入函数内部修改,这也是通过将上面的代码改造成职责链,整个代码的逻辑就相对清晰了,并且如果此时要增加其他的处理逻辑,只需要增加其中的节点,然后重新设置链中相关节点的顺序即可。当前在JavaScript中我们也可以利用AOP的方式实现职责连,更加的简便
如下图,职责链模式能够很好的耦合请求发送者和接受请求者之前复杂的关系,但是存在明显的缺点,首先我们不能保证请求在职责链中一定会得到处理,因此我们最好在职责链末尾增加相应的步骤保底。并且职责链在程序中会增加节点,而大部分节点并没有实际的作用,从性能方面考虑,过长的职责链可能会带来性能的损耗。
中介者模式
中介者模式主要用来解除对象与对象之间N:N复杂的耦合关系,通过引入中介者,所有的对象都只与中介者对象关联,将复杂的N:N网状关系变为相对简单的1:N关联。
通过引入中介者对象,所有的节点对象只跟中介者通信。当下拉选择框colorSelect、memorySelect和文本输入框numberInput发生了事件行为时,它们仅仅通知中介者它们被改变了,同时把自身当作参数传入中介者,以便中介者辨别是谁发生了改变。剩下的所有事情都交给中介者对象来完成,这样一来,无论是修改还是新增节点,都只需要改动中介者对象里的代码。
中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。中介者模式也存在一些缺点。其中,最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。
备注:上面代码来源于曾探的《JavaScript设计模式与开发实践》一书,描述的非常生动形象,非常推荐大家阅读,上面仅作为是本人的学习总结,望共同进步。