开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第20天,点击查看活动详情。
行为型模式
行为型模式是将不同的行为代码解耦,从而解决特定场景问题的一些经典结构。
行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型设计模式比较多,有 11 个,几乎占了 23 种经典设计模式的一半。它们分别是:观察者模式、模板模式、策略模式、职责链模式、状态模式、迭代器模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。
观察者模式(发布/订阅模式)
观察者模式又叫发布订阅模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
优点 解耦。
- 时间上的解耦:注册的订阅行为由消息的发布方来决定何时调用,订阅者不用持续关注,当消息发生时发布者会负责通知;
- 对象上的解耦 :发布者不用提前知道消息的接受者是谁,发布者只需要遍历处理所有订阅该消息类型的订阅者发送消息即可(迭代器模式),由此解耦了发布者和订阅者之间的联系,互不持有,都依赖于抽象,不再依赖于具体;
缺点
- 增加消耗:创建结构和缓存订阅者这两个过程需要消耗计算和内存资源,即使订阅后始终没有触发,订阅者也会始终存在于内存;
- 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的 deps/subs/watchers
缺点主要在于理解成本、运行效率、资源消耗,特别是在多级发布 - 订阅时,情况会变得更复杂。
这个还是有点理解的,因为会见到这个关键词。
理解这个模式的话可以想一下回调函数,我们在一个方法调用的时候传入另一个方法,然后在目标函数执行结束以后,触发回调函数。而观察者模式则是在此基础上进行了抽象化,将两个函数变成了两个对象,当一个对象发生变化的时候自动的去触发另一个对象里面的方法。这样是不是就很容易理解了?
先来一个简单的例子:
// 观察目标
class Subject {
constructor() {
this.observers = [];
}
// 添加一个观察者
add(observer) {
this.observers.push(observer);
}
// 通知观察者
notify() {
console.log("subject : 我先做我自己的事情!");
this.observers.forEach((item) => item.update());
}
}
// 观察者
class Observer {
constructor(name) {
this.name = name;
}
// 默认被触发事件
update() {
console.log(this.name, ": 我被触发了");
}
}
const observer1 = new Observer("张三丰");
const observer2 = new Observer("张无忌");
const subject = new Subject();
subject.add(observer1);
subject.add(observer2);
subject.notify();
上面这个小栗子不知道好不好理解,一开始我是对这个例子不感兴趣的,后来琢磨了一下还是挺有意思的。
常言道,万变不离其宗,这个例子就是观察者模式的核心思路:
- 需要被经常操作的是 Subject, Observer 可以作为一系列方法,例如:操作dom、发起请求、修改视图等;
- 尽量提高 Observer 功能的单一化,精简化,避免使用繁杂的结构使简单事情复杂化;
- Subject 可以给看做一个状态机,Observer 则是响应器,状态变化触发响应;
- 正如开头所说,Subject 就是目标函数,而 Observer 则可以理解为回调函数,不过是在实例化对象的时候就已经绑定好了。
vue 数据双向绑定原理:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" id="app">
<span id="childSpan"></span>
</body>
<script>
var obj = {}
var initValue='初始值'
Object.defineProperty(obj,'initValue',{
get(){
console.log('获取obj最新的值');
return initValue
},
set(newVal){
initValue = newVal
console.log('设置最新的值');
// 获取到最新的值 然后将最新的值赋值给我们的span
document.getElementById('childSpan').innerHTML = initValue
console.log(obj.initValue);
}
})
document.addEventListener('keyup', function (e) {
obj.initValue = e.target.value; //监听文本框里面的值 获取最新的值 然后赋值给obj
})
</script>
</html>
如上所示,是将所有的枝叶剪除以后的核心逻辑。
这里 观察目标则是obj,而观察者则是js语言的特性 Object.defineProperty 监听数据的 get 和 set 方法。
迭代器模式
迭代器模式:用于顺序地访问聚合对象内部的元素,又无需知道对象内部结构。使用了迭代器之后,使用者不需要关心对象的内部构造,就可以按序访问其中的每个元素。
这个几乎每天都在用哈!
例如这段诡异的代码:
initTreeTable(_map, data, _i) {
return `<table>
<tr><th colspan="2"></th>${_map
.map((k) => `<th>${data[0].result[k].label}</th>`)
.join('')}</tr>
${data
.map(
(_d) => `
<tr>
<th rowspan="${_i === 1 ? 3 : 2}">${_d.label.split('-')[1]}</th>
<th>均分</th>${_map
.map((k) => `<td>${_d.result[k].average}</td>`)
.join('')}
</tr>
<tr>
<th>标准差</th>
${_map.map((k) => `<td>${_d.result[k].standard}</td>`).join('')}
</tr>
${
_i == 1
? `<tr>
<th>超常占比</th>
${_map
.map((k) => `<td>${_d.result[k].supernormal}</td>`)
.join('')}
</tr>`
: ''
}
`
)
.join('')}
</table>`
}
那么有个小问题,不使用 js 中 array 的方法,如何去遍历一个数组呢?
--递归
策略模式
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
优点
- 策略之间相互独立,但策略可以自由切换 ,这个策略模式的特点给策略模式带来很多灵活性,也提高了策略的复用率;
- 如果不采用策略模式,那么在选策略时一般会采用多重的条件判断,采用策略模式可以 避免多重条件判断,增加可维护性;
- 可扩展性好,策略可以很方便的进行扩展;
缺点
- 策略相互独立,因此一些复杂的算法逻辑 无法共享,造成一些资源浪费;
- 如果用户想采用什么策略,必须了解策略的实现,因此,所有策略都需向外暴露,这是违背迪米特法则/最少知识原则的,也增加了用户对策略对象的使用成本;
如果说 迭代器模式 是在优化遍历场景的话,策略模式则是在 优化 分支语句场景。
学习 js 的时候我们是这个学习过程吧:
- 使用 if 判断,如果跳出则 return
- 使用 if + else if,如果跳出 则 return
- 使用 switch + case,进行判断
- 配置一个 json ,使用 object[key] 的方式执行或取值
而策略模式其实就是在有效的解决上述这些方法解决起来会显得很臃肿的一些实际场景中。
最常见的场景就是表单验证了,其实我们验证的是个字符串,并不是手机号、email、身份证号...
把一些验证方法封装起来,把 字符串 传进去,你告诉我过不过就完了!!!
但是有一个致命的缺陷:文档一定要维护好,不然的话今天写的工具,明天自己都不会用了[苦笑]!
模板方法模式
模板方法模式:父类中定义一组操作算法骨架,而将一些实现步骤延迟到子类中,使得子类可以不改变父类的算法结构的同时,重新定义算法中的某些实现步骤。模板方法模式的关键是算法步骤的骨架和具体实现分离。
没看明白在干嘛,感觉就是个构造器。
不过确实在工作中经常会用到,网上的例子都是冲咖啡,等发现好例子了再补充过来!