创建型设计模式
1 单例模式
一个类只有一个实例,并提供一个访问它的全局访问点。
js
class LoginForm {
constructor() {
this.state = 'hide'
}
show() {
if (this.state === 'show') {
alert('已经显示')
return
}
this.state = 'show'
console.log('登录框显示成功')
}
hide() {
if (this.state === 'hide') {
alert('已经隐藏')
return
}
this.state = 'hide'
console.log('登录框隐藏成功')
}
}
LoginForm.getInstance = (function () {
let instance
return function () {
if (!instance) {
instance = new LoginForm()
}
return instance
}
})()
let obj1 = LoginForm.getInstance()
obj1.show()
let obj2 = LoginForm.getInstance()
obj2.hide()
console.log(obj1 === obj2)
优点:
- 划分命名空间,减少全局变量
- 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
- 只会实例化一次。简化了代码的调试和维护
缺点:
- 由于单例模式提供的是一种单点访问,所以可能导致模块间强耦合
- 不利于单元测试,无法单独测试一个调用了来自单例的方法的类,只能把它与那个单例作为一个单元进行测试
应用场景:
- 定义命名空间和实现分支型方法
- 登陆框
- vuex和redux中的store
2 原型模式
原型模式就是指创建一个共享的原型,通过拷贝这个原型来创建新的类,用于创建重复对象,带来性能上的提升。
js
class Person {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Student extends Person {
constructor(name) {
super(name)
}
sayHello() {
console.log(`Hello, My name is ${this.name}`)
}
}
let student = new Student("xiaoming")
student.sayHello()
3 工厂模式
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
js
class Product {
constructor(name) {
this.name = name
}
init() {
console.log('init')
}
fun() {
console.log('fun')
}
}
class Factory {
create(name) {
return new Product(name)
}
}
// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()
优点:
- 创建对象的过程可能比较复杂,但是我们只需要关心创建结果
- 构造函数和创建者分离,符合'开闭原则'
- 一个调用者想创建一个对象只要知道其名称就可以了
- 扩展性强,想增加一个产品,只要扩展一个工厂类就可以了
缺点:
- 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统复杂度
- 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
适用:
- 不想让子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话
- 将new操作简单封装,遇到new的时候就应该考虑是否使用工厂模式
- 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这是就可以考虑工厂模式,简化实现,有利于消除对象间的耦合,提供更大的灵活性
应用场景:
- jQuery的$()就是一个工厂函数,它根据传入的参数的不同创建元素,或者去寻找上下文中的元素,创建相应的jQuery对象。
- Vue的异步组件,Vue使用一个工厂函数来定义组件,这个工厂函数异步解析组件定义。Vue只有在这个组件被渲染的时候才触发工厂函数,且会把结果缓存起来供未来重渲染。
结构型设计模式
1 适配器模式
将一个类的接口转化为另外一个接口以满足用户需求,使类之间接口不兼容问题通过适配器得以解决。
js
class Plug {
getName() {
return 'iphone充电头';
}
}
class Target {
constructor() {
this.plug = new Plug();
}
getName() {
return this.plug.getName() + ' 适配器Type-c充电头';
}
}
let target = new Target();
target.getName(); // iphone充电头 适配器转Type-c充电头
优点:
- 可以让任何两个没有关联的类一起运行
- 提高了类的复用
- 适配对象,适配库,适配数据
缺点:
- 额外对象的创建非直接调用,存在一定的开销
- 没必要使用适配器模式的时候考虑重构,使用的话尽量把文档完善
应用场景:
- 整合第三方SDK
- 封装旧接口,比如对ajax的封装
- vue的计算属性,原有data不满足当前需求,使用计算属性适配成我们想要的格式,对原数据没有更改。
适配器模式提供了一个不同的接口;代理模式提供一个一模一样的接口
2 装饰器模式
动态地给某个对象添加一些额外的职责,是一种实现继承的替代方案。在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户更复杂的需求,而不会影响从这个类中派生的其他对象。
js
class Cellphone {
create() {
console.log('生成一个手机')
}
}
class Decorator {
constructor(cellphone) {
this.cellphone = cellphone
}
create() {
this.cellphone.create()
this.createShell(cellphone)
}
createShell() {
console.log('生成手机壳')
}
}
// 测试代码
let cellphone = new Cellphone()
cellphone.create()
let dec = new Decorator(cellphone)
dec.create()
优点:
- 装饰类和被装饰类都只关心自身的核心业务,实现了解耦
- 方便动态扩展功能,且提供了比继承更多的灵活性
缺点:
- 多层装饰比较复杂
- 常常会引入许多小对象,看起来相似却功能不同,从而使我们的程序架构变得复杂起来
应用场景:
比如现在有4 种型号的自行车,我们为每种自行车都定义了一个单独的类。现在要给每种自行车都装上前灯、尾灯和铃铛这3 种配件。如果使用继承的方式来给每种自行车创建子类,则需要 4×3 = 12 个子类。但是如果把前灯、尾灯、铃铛这些对象动态组合到自行车上面,则只需要额外增加3 个类
3 代理模式
为一个对象提供一个代用品或占位符,以便控制对它的访问。
假设当A 在心情好的时候收到花,小明表白成功的几率有60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。但是A 的朋友B 却很了解A,所以小明只管把花交给B,B 会监听A 的心情变化,然后选择A 心情好的时候把花转交给A,代码如下:
js
let Flower = function() {}
let xiaoming = {
sendFlower: function(target) {
let flower = new Flower()
target.receiveFlower(flower)
}
}
let B = {
receiveFlower: function(flower) {
A.listenGoodMood(function() {
A.receiveFlower(flower)
})
}
}
let A = {
receiveFlower: function(flower) {
console.log('收到花'+ flower)
},
listenGoodMood: function(fn) {
setTimeout(function() {
fn()
}, 1000)
}
}
xiaoming.sendFlower(B)
优点:
- 代理模式可以将代理对象与被调用对象分离,降低系统耦合度。在客户端和目标对象之间起到一个中介的作用,可以起到保护目标对象的作用。
- 代理对象可以扩展目标对象的功能,通过修改代理对象就可以实现,符合开闭原则。
缺点:
处理请求速度可能有差别,非直接访问存在开销
应用场景:
- HTML元素事件代理
- proxy
与装饰器模式的区别:
- 装饰器模式:扩展功能,原有功能不变且可直接使用
- 代理模式:显示原有功能,但是经过限制之后的
行为型设计模式
1 观察者模式
定义一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个对象的状态发生变化就会通知所有观察者对象,使他们能够自动更新自己,当一个对象的改变需要同时改变其他对象,并且它不知道具体多少对象需要改变的时候就可以使用观察者模式。
js
document.body.addEventListener('click', function() {
console.log('hello world!');
});
document.body.click()
优点:
- 支持简单的广播通信,自动通知所有已经订阅过的对象
- 目标对象与观察者之间的抽象耦合关系能单独扩展以及复用
- 增加了灵活性
- 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化
缺点:
过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解。
2 策略模式
定义一系列算法,把他们一个个封装起来,并且使它们可以互相替换
优点:
- 利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
- 提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展
- 利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案
缺点:
- 会在程序中增加许多策略类或者策略对象
- 要使用策略模式,必须了解所有strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy
3 职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链并沿着这条链传递该请求,直到有一个对象处理它为止。
js
// 请假审批,需要组长审批、经理审批、总监审批
class Action {
constructor(name) {
this.name = name
this.nextAction = null
}
setNextAction(action) {
this.nextAction = action
}
handle() {
console.log( `${this.name} 审批`)
if (this.nextAction != null) {
this.nextAction.handle()
}
}
}
let a1 = new Action("组长")
let a2 = new Action("经理")
let a3 = new Action("总监")
a1.setNextAction(a2)
a2.setNextAction(a3)
a1.handle()
优点:
- 降低耦合度。将请求的发送者和接收者解耦
- 简化了对象。使得对象不需要知道链的结构
- 增强给对象指派职责的灵活性。
- 增加新的请求处理类很方便
缺点:
- 不能保证某个请求一定会被链中的节点处理,这种情况可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。
- 使程序中多了很多节点对象,可能再一次请求的过程中,大部分的节点并没有起到实质性的作用。他们的作用仅仅是让请求传递下去,从性能当面考虑,要避免过长的职责链到来的性能损耗。