《JavaScript设计模式》----张荣铭(三)
首先说一下什么是设计模式?以及我们为什么要学习设计模式?
设计模式的定义是:设计模式是在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案
也可以通俗的理解为:设计模式在某个特定场景下都某种问题的解决方案。当然,这也就是为什么我们要学习设计模式的原因。本书将设计模式按照类型分成六大类
- 创建型设计模式
- 结构型设计模式
- 行为型设计模式
- 技巧型设计模式
- 架构型设计模式
3 行为型设计模式
行为型设计模式用于不同对象之间职责划分或者算法抽象,行为型设计模式不仅仅涉及类和对象,还设计类或对象之间的交流模式并加以实现。
3.1 模板方法模式
模板方法模式:父类中定义一组操作算法骨架,而将一些实现步骤延迟到子类中,使得子类可以不改变父类的算法结构的同时可重新定义算法中的某些实现步骤。
比如公司有各种各样的弹框,但是为了做到样式大体上统一,那么可以创建一个基类(也就是模板),让其他弹框去继承这个基类,然后按需修改样式。(有点类似于我们的组件开发,需子类重新定义的部分可以暴露出配置项让使用者修改
3.2 观察者模式和发布-订阅模式
主要解决问题:模块间通信问题
3.2.1 观察者模式
观察者模式指的是一个对象(Subject)维护了一系列依赖于它的对象(Observer),当这个对象发生改变的时候Subject对象则通知一系列Observer对象进行更新。
function Subject() {
this.observers = [];
}
Subject.prototype = {
add: function (observer) {
this.observers.push(observer);
},
remove: function (observer) {
var observers = this.observers;
observers.forEach((item, index) => {
// 不严谨,最好给个 唯一标识
item.name === observer.name;
observers.splice(index, 1);
})
},
notify: function (args) {
var observers = this.observers;
observers.forEach((item) => {
item.update(args);
})
}
}
function Observer(name) {
this.name = name;
}
Observer.prototype = {
update: function (args) {
console.log(`${this.name} updated args: ${JSON.stringify(args)}`)
}
}
var sub = new Subject();
var obs1 = new Observer('ttsy1');
var obs2 = new Observer('ttsy2');
sub.add(obs1);
sub.add(obs2);
sub.notify('我发生改变了!');
3.2.2 发布-订阅模式
发布-订阅模式中,发布者和订阅者之间多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件(也可以理解成是一个消息中心)
以此避免发布者和订阅者之间产生依赖关系
function MessageCenter() {
var _message = {};
// 注册 (只需要告诉消息中心要注册什么样类型的信息)
this.regist = function(msgType) {
if(typeof _message[msgType] === 'undifined') {
_message[msgType] = []; // 这里说明 msgType这个msg已经注册成功了,并且维护一个数组来保存订阅者函数subFn(理解成要干的事情)!
} else {
console.log('你这个msgType:' + msgType + '已经被注册了!');
}
}
// 订阅 (要告诉消息中心,要订阅什么样的信息,并且一旦消息发布,要干什么)
this.subscribe = function(msgType,subFn) {
// 同理,首先判断有没有这个消息可以订阅
if(typeof _messages[msgType] !== 'undefined'){
_messages[msgType].push(subFn); // 要干的事情放在 _message[msgType]这个数组中
}else{
console.log('这个消息还没注册过,无法订阅')
}
}
//发布 (要发布某条消息,并通知所有订阅了的订阅者函数)
this.fire = function(msgType,args) {
// 同理,判断下有没有这个消息可以发布
if(typeof _messages[msgType] === 'undefined') {
console.log('没有这条消息,无法发布');
return false;
}
// subFn在执行的时候,可能需要一些额外的参数,通过args传入
var events = {
type: msgType,
args: args || {}
}
_message[msgType].map( subFn => {
subFn(events)
})
}
}
发布-订阅的使用
// 首先 声明一个消息中心
var msgCenter = new MessageCenter();
// 注册
msgCenter.regist('diff');
// 订阅 订阅函数为subscribeFn()
msgCenter.subscribe('diff',subscribeFn);
function subscribeFn(events) {
console.log(events.type,events.args);
}
// 注意了!我要发布了!
setTimeout(function () {
msgCenter.fire('diff','fire msg')
}, 1000)
3.2.3 发布订阅和观察者模式的区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z9WAMYTO-1598369805654)(https://imgkr2.cn-bj.ufileos.com/da5f7cf8-15d7-41f1-bbdd-5e112a8ddf68.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=B1XA%252F6Sh6ZFunAiCqVLCLuV6JWQ%253D&Expires=1597295891)]
- 在Observer模式中,Observers知道Subject,同时Subject还保留了Observers的记录。然而,在发布者/订阅者中,发布者和订阅者不需要彼此了解。他们只是在消息队列或代理的帮助下进行通信。
- 在Publisher / Subscriber模式中,组件是松散耦合的,而不是Observer模式。
- 观察者模式主要以同步方式实现,即当某些事件发生时,Subject调用其所有观察者的适当方法。发布者/订阅者在大多情况下是异步方式(使用消息队列)。
3.3 状态模式
状态模式:状态模式既是解决程序中臃肿的分支判断语句问题,将每个分支转化为一种状态独立出来,方便每种状态的管理又不至于每次执行时遍历所有分支。
比如我们玩超级玛丽这个游戏,有跳跃、开枪、蹲下、奔跑等动作(多个动作可以同时进行),那么使用if或者switch分支判断去实现,增加的成本是无法想象的。
var lastAction = '';
function changeMarry(action) {
if (action === 'jump') {
// 跳跃动作
} else if (action === 'move') {
// 移动动作
} else if (action === 'run') {
//
} ...
// 除此之外还要加上多个动作的排列组合
}
使用状态模式玩超级玛丽
var marryState = function () {
var _currentState = {};
// 状态模式
var states = {
jump: function () {
console.log('jump');
},
move: function () {
console.log('move');
},
shoot: function () {
console.log('shoot');
},
squat: function () {
console.log('squat');
}
}
var Action = {
changeState: function () {
var args = arguments;
_currentState = {};
if (args.length) {
// args.forEach((item, index) => {
// _currentState[args[index]] = true;
// });
for (var i = 0; i < args.length; i++) {
_currentState[args[i]] = true;
}
}
// 链式调用
return this;
},
goes: function () {
console.log('触发一次动作:', Object.keys(_currentState));
Object.keys(_currentState).forEach(item => {
states[item] && states[item]();
})
// 链式调用
return this;
}
};
return {
change: Action.changeState,
goes: Action.goes
}
}
var marry = new marryState();
marry.change('jump', 'shoot')
.goes()
.goes()
.change('squat', 'shoot')
.goes()
3.4 策略模式
策略模式:将定义的一组算法封装起来,使其相互之间可以替换。封装的算法具有一定独立性,不会随客户端改变而改变。(策略模式使得算法脱离于模块逻辑而独立管理)
策略模式和状态模式的区别: 策略模式也是一种优化分支判断语句的模式,但策略模式不需要管理状态,状态(也就是策略)中没有依赖关系、策略之间可以相互替换,策略对象内部保存的是相互独立的算法
下面使用策略模式封装一个表单验证类
var InputStrategy = function () {
var strategy = {
required: function (value) {
return value ? '' : console.log('不能为空');
},
number: function (value) {
return /^[0-9]+(\.[0-9]+)?$/.test(value) ? '' : console.log('请输入数字');
}
// ...
};
return {
check: function (type, value) {
return strategy[type] ? strategy[type](value) : console.log('没有该类型的检测方法');
},
addStrategy: function (type, fn) {
strategy[type] = fn;
}
}
}
var inputStrategy = new InputStrategy();
inputStrategy.check('required', undefined);
inputStrategy.check('number', '1231232');
3.5 职责链模式
职责链模式:职责链模式是使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。
简而言之,请求以后,从第一个对象开始,链中收到请求的对象要么亲自处理它,要么转发给链中下一个候选者。提交请求的对象并不明确知道哪一个对象会处理它–也就是该请求有一个隐式的接受者。根据运行时刻,任意候选者都可以响应相应的请求,候选者的数目是任意的,可以在运行时刻决定哪些候选者参与到链中。
核心思想: 请求者不必知道是谁哪个节点对象处理的请求。如果当前不符合终止条件,那么把请求转发给下一个节点处理
举个例子,我们现在有个需求,要根据圆的半径尺寸画不同大小的圆
优化前:我们正常情况下会这样做
const Circle = function () {
this.drawByRadius = function (radius) {
if (radius < 5) {
this.drawSmallCircle(radius);
} else if (radius >= 5 || radius <= 15) {
this.drawMediumCircle(radius);
} else if (radius > 15) {
this.drawLargeCircle(radius);
}
};
this.drawSmallCircle = function (radius) {
// ...一系列复杂的操作
console.log(`画了一个${radius}的小型圆`);
};
this.drawMediumCircle = function (radius) {
// ...一系列复杂的操作
console.log(`画了一个${radius}的中型圆`);
};
this.drawLargeCircle = function (radius) {
// ...一系列复杂的操作
console.log(`画了一个${radius}的大型圆`);
};
};
首先这样做的画,drawByRadius这个方法职责锅中,每次修改我们的画大中小圆的半径限制的时候都需要去修改代码,不符合开关原则。
优化后: 使用职责链模式: 拆封每个方法为单独的处理者类(每个类单独负责画大,中,小圆),每个处理者类保存下一个处理类的引用,这样每次draw的话程序都会挨个执行职责链上都每一个方法
const drawSmallCircle = function (min, max) {
this.min = min;
this.max = max;
this.setNextReceiver = function (nextReceiver) {
this.nextReceiver = nextReceiver;
};
this.draw = function (radius) {
console.log("执行:drawSmallCircle");
if (this.min < radius && radius < this.max) {
console.log(`画了一个${radius}的小型圆`);
}
// 其实这里我倒是觉得 如果成功执行了 就没必要执行下一个处理类
this.nextReceiver ? this.nextReceiver.draw(radius) : "";
};
};
const drawMediumCircle = function (min, max) {
this.min = min;
this.max = max;
this.setNextReceiver = function (nextReceiver) {
this.nextReceiver = nextReceiver;
};
this.draw = function (radius) {
console.log("执行:drawMediumCircle");
if (this.min < radius && radius < this.max) {
console.log(`画了一个${radius}的中型圆`);
}
// 其实这里我倒是觉得 如果成功执行了 就没必要执行下一个处理类
this.nextReceiver ? this.nextReceiver.draw(radius) : "";
};
};
const drawLargeCircle = function (min, max) {
this.min = min;
this.max = max;
this.setNextReceiver = function (nextReceiver) {
this.nextReceiver = nextReceiver;
};
this.draw = function (radius) {
console.log("执行:drawLargeCircle");
if (this.min < radius && radius < this.max) {
console.log(`画了一个${radius}的大型圆`);
}
// 其实这里我倒是觉得 如果成功执行了 就没必要执行下一个处理类
this.nextReceiver ? this.nextReceiver.draw(radius) : "";
};
};
function initChain() {
const smallCircle = new drawSmallCircle(0, 10); // 这里的参数可以直接修改,比优化前要好
const mediumCircle = new drawMediumCircle(10, 20);
const bigCircle = new drawLargeCircle(20, 100);
smallCircle.setNextReceiver(mediumCircle);
mediumCircle.setNextReceiver(bigCircle);
return smallCircle;
}
var chainDrawCircle = new initChain();
chainDrawCircle.draw(13);
这样优化的结果是,我们可以通过参数去控制我们的大中小圆的半径限制,另外,我们不需要知道我们使用哪个特定的类去处理我们的请求,会挨个执行职责链上的每一个方法(不过我认为,像画圆这种只要执行了一次就可以return,没必要挨个再执行一遍,我的例子不大好)
除此之外,由于每个接收者都是一个单独的对象,方便我们做单元测试。
3.6 命令模式
命令模式:命令模式是一种松耦合的程序设计,使得请求的发送者和请求的接受者能够消除彼此之间的耦合关系。
命令模式中的三个主体:
- 发送者(Invoker)
- 命令对象(Command)
- 接受对象(Receiver)
举个现实场景中的例子:我去餐厅吃饭,然后点了三个菜让服务员递给厨师,厨师就按照菜单上的三个菜名,kakaka就把我的菜给我整好了。相应的角色对应我们的命令模式如下:
请求发送者(我)-> 命令对象(服务员手中的单子)-> 请求接受者(厨师)
举个例子:比如在战场上打仗,将军不会去直接命令士兵,而且是通过小号手/其他途径来传达给士兵。
class Receiver {
exec() {
console.log("执行");
}
}
class Command {
constructor(receiver) {
this.receiver = receiver;
}
cmd() {
console.log("触发命令");
this.receiver.exec();
}
}
class Invoker {
constructor(command) {
this.command = command;
}
invoke() {
console.log("开始");
this.command.cmd();
}
}
// 士兵(Receiver)
let soldier = new Receiver();
// 小号手(Command)
let trumpeter = new Command(soldier);
// 将军 (Invoker)
let general = new Invoker(trumpeter);
general.invoke();
3.7 访问者模式
访问者模式是将对数据的操作和数据结构进行分离,将对数据中各元素的操作封装成独立的类,使其在不改变数据结构的前提下可以拓展对数据新的操作。
// 访问者
class Visitor {
constructor() {}
visitConcreteElement(ConcreteElement) {
ConcreteElement.operation()
}
}
// 元素类
class ConcreteElement{
constructor() {
}
operation() {
console.log("ConcreteElement.operation invoked");
}
accept(visitor) {
visitor.visitConcreteElement(this)
}
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
elementA.accept(visitor)
3.8 中介者模式
通过中介者对象封装一系列对象之间的交互,是对象之间不再相互引用,降低他们之间的耦合。有时中介者对象也可以改变对象之间的交互。
// 声明一个中介者对象
// 我们的一系列操作不直接导致页面发生改变,而是通过这个中介者对象来分发(一方面是为了操作对象和被操作对象解耦,二来我们可以在中介者对象中对一些额外的操作)
var colorSelect = document.getElementById("colorSelect");
var memorySelect = document.getElementById("memorySelect");
var numSelect = document.getElementById("numSelect");
var Mediator = function () {
this.operateCount = 0;
(this.changed = function (obj) {
switch (obj) {
case "colorSelect":
// 响应操作
// 顺便记录下操作的总数
this.operateCount += 1;
break;
case "memorySelect":
// 响应操作
// 顺便记录下操作的总数
this.operateCount += 1;
break;
case "numSelect":
// 响应操作
// 顺便记录下操作的总数
this.operateCount += 1;
break;
}
}),
(this.getOperatCount = function () {
return this.operateCount;
});
};
var mediator1 = new Mediator();
colorSelect.onclick = function () {
mediator1.changed("colorSelect");
console.log(mediator1.getOperatCount());
};
memorySelect.onclick = function () {
mediator1.changed("memorySelect");
console.log(mediator1.getOperatCount());
};
numSelect.onclick = function () {
mediator1.changed("numSelect");
console.log(mediator1.getOperatCount());
};
3.9 备忘录模式
//备忘类
class Memento{
constructor(content){
this.content = content
}
getContent(){
return this.content
}
}
// 备忘列表
class CareTaker {
constructor(){
this.list = []
}
add(memento){
this.list.push(memento)
}
get(index){
return this.list[index]
}
}
// 编辑器
class Editor {
constructor(){
this.content = null
}
setContent(content){
this.content = content
}
getContent(){
return this.content
}
saveContentToMemento(){
return new Memento(this.content)
}
getContentFromMemento(memento){
this.content = memento.getContent()
}
}
//测试代码
let editor = new Editor()
let careTaker = new CareTaker()
editor.setContent('111')
editor.setContent('222')
careTaker.add(editor.saveContentToMemento())
editor.setContent('333')
careTaker.add(editor.saveContentToMemento())
editor.setContent('444')
console.log(editor.getContent()) //444
editor.getContentFromMemento(careTaker.get(1))
console.log(editor.getContent()) //333
editor.getContentFromMemento(careTaker.get(0))
console.log(editor.getContent()) //222
看了上面的例子我不禁想到canvas中restore方法,正是用了备忘录模式!
优点:给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态
缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
3.10 迭代器模式
提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。
也就是给一个复杂的可迭代的对象封装了简易的API以访问内部变量。
暂不提供案例
3.11 解释器模式
对于一种语言,给出其文法表示形式,并定义一种解释器,通过这种解释器来解释语句中定义的句子。
暂不提供案例