探讨一些经典和现代的设计模式的 JavaScript 实现。
没有最好的,只有最合适的
设计模式一共分为 3 大类 23 种
模式类型 | 设计模式 |
---|---|
创建型模式 | 单例模式、工厂模式、建造者模式 |
结构型模式 | 适配器模式、装饰器模式、代理模式 |
行为型模式 | 策略模式、观察者模式、发布订阅模式、职责链模式、中介者模式 |
单例模式#
一个类只有一个实例,并提供一个访问他的全局访问点
-
Singleton
:特定类,这是我们需要访问的类,访问者要拿到的是它的实例; -
instance
:单例,是特定类的实例,特定类一般会提供getInstance
方法来获取该单例; -
getInstance
:获取单例的方法;
class Singleton {
let _instance = null;
static getInstance() {
if (!Singleton._instance) {
Singleton.instance = new Singleton()
}
// 如果这个唯一的实例已经存在,则直接返回
return Singleton._instance
}
}
const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()
console.log(s1 === s2) // true
实例#
Vuex 实现了一个全局的 store 用来存储应用的所有状态。这个 store 的实现就是单例模式的典型应用。
使用场景#
-
如果一个类实例化过程消耗资源比较多,可以使用单例避免性能浪费
-
需要公共状态,可以使用单例保证访问一致性。
工厂模式#
工厂模式:根据不同的参数,返回不同类的实例。将对象的创建与对象的实现分离。实现复杂,但使用简单。直接使用工厂提供的方法即可
优点:
-
良好的封装,访问者无需了解创建过程,代码结构清晰。
-
扩展性良好,通过工厂方法隔离了用户和创建流程,符合开闭原则。
缺点:
给系统增加了抽象性,带来了额外的系统复杂度,不能滥用
实例#
document.createElement
创建 DOM
元素。这个方法采用的就是工厂模式,方法内部很复杂,但外部使用很简单。
使用场景#
-
对象创建比较复杂,访问者无需了解创建过程。
-
需要处理大量具有相同 / 类似属性的小对象。
适配器模式#
用于解决兼容问题,接口 / 方法 / 数据不兼容,将其转换成访问者期望的格式进行使用。
场景特点:
- 整合第三方 SDK
- 封装旧接口
装饰器模式#
- 动态地给某个对象添加一些额外的职责,,是一种实现继承的替代方案
- 在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象
有点原型链的味道
代理模式#
为一个对象提供一个代用品或占位符,以便控制对它的访问
使用场景#
- ES6 的 proxy
- jQuery.proxy () 方法
装饰者与代理模式的区别#
- 装饰者模式: 扩展功能,原有功能不变且可直接使用
- 代理模式: 显示原有功能,但是经过限制之后的
策略模式#
定义一系列算法,根据输入的参数决定使用哪个算法。
实例#
场景:双十一满减活动。满 200-20、满 300-50、满 500-100。这个需求,怎么写?
// if-else:臃肿,难改动
function priceCalculate(discountType,price){
if(discountType === 'discount200-20'){
return price - Math.floor(price/200) * 20;
}else if(discountType === 'discount300-50'){
return price - Math.floor(price/300) * 50;
}else if(userType === 'discount500-100'){
return price - Math.floor(price/500) * 100;
}
}
//策略模式改写,隐藏了算法,预留了增加策略的入口,便于拓展
const priceCalculate = (function(){
const discountMap = {
'discount200-20': function(price) {
return price - Math.floor(price / 200) * 20;
},
'discount300-50': function(price) {
return price - Math.floor(price/300) * 50;
},
'discount500-100': function(price) {
return price - Math.floor(price/500) * 100;
},
};
return {
addStategy(stategyName,fn){
if(discountMap[stategyName]) return;
discountMap[stategyName] = fn;
},
priceCal(discountType,price){
return discountMap[discountType] && discountMap[discountType](price);
}
}
})()
优点:
- 策略相互独立,可以互相切换。提高了灵活性以及复用性。
- 不需要使用
if-else
进行策略选择,提高了维护性。 - 可扩展性好,满足开闭原则。
缺点:
- 策略相互独立,一些复杂的算法逻辑无法共享,造成资源浪费。
- 用户在使用策略时,需要了解具体的策略实现。不满足最少知识原则,增加了使用成本。
使用场景#
- 算法需要自由切换的场景。
- 多个算法只有行为上有些不同,可以考虑策略模式动态选择算法。
- 需要多重判断,可以考虑策略模式规避多重条件判断。
观察者模式#
一个对象(称为 subject)维持一系列依赖于它的对象(称为 observer),将有关状态的任何变更自动通知给它们(观察者)。
优缺点#
优点:目标变化就会通知观察者,这是观察者模式最大的优点。
缺点: 目标和观察者是耦合在一起的,要实现观察者模式,必须同时引入被观察者和观察者才能达到响应式的效果。
使用场景#
假设 B 站用户就是观察者,B 站 up 主是被观察者,有多个的 B 站用户关注了青春湖北这个 up 主,当这个 up 主更新视频时就会通知这些关注的 B 站用户。
发布订阅模式#
基于一个主题,希望接收通知的对象(称为 subscriber)通过自定义事件订阅主题,被激活事件的对象(称为 publisher)通过发布主题事件的方式被通知。
使用场景#
微信会关注很多公众号,公众号有新文章发布时,就会有消息及时通知我们文章更新了。
这个时候公众号为发布者,用户为订阅者,用户将订阅公众号的事件注册到事件调度中心,当发布者发布新文章时,会发布事件至事件调度中心,调度中心会发消息告诉订阅者。
Vue 双向绑定中的发布订阅模式#
Vue
双向绑定通过数据劫持和发布 - 订阅模式实现
-
通过
DefineProperty
劫持各个数据的setter
和getter
,并为每个数据添加一个订阅者列表,这个列表将会记录所有依赖这个数据的组件。响应式数据相当于消息的发布者。
-
每个组件都对应一个
Watcher
订阅者,当组件渲染函数执行时,会将本组件的Watcher
加入到所依赖的响应式数据的订阅者列表中。这个过程叫做 “依赖收集”。
-
当响应式数据发生变化时,会出
setter
,setter
负责通知数据的订阅者列表中的Watcher
,Watcher
触发组件重新渲染来更新视图。视图层相当于消息的订阅者。
观察者模式和发布订阅的区别#
观察者是经典软件设计模式
中的一种,但发布订阅只是软件架构中的一种消息范式
观察者模式 | 发布订阅 |
---|---|
2 个角色 | 3 个角色 |
重点是被观察者 | 重点是发布订阅中心 |
观察与被观察的关系是通过被观察者主动
建立的,被观察者
至少要有三个方法 —— 添加观察者、移除观察者、通知观察者。
发布订阅基于一个中心来建立整个体系,其中发布者
和订阅者
不直接进行通信,而是发布者将要发布的消息交由中心管理,订阅者也是根据自己的情况,按需订阅中心中的消息。
发布订阅的实现内部利用了观察者模式
,但由于发布订阅中心
这一中间层的出现,对于生产方和消费方的通信管理变得更加的可管理和可拓展。