设计模式

6/4/2020 设计模式

# 设计模式

设计模式有多种,以下为几种比较常见的

设计模式 描述 例子
单例模式 一个类只能构造出唯一实例 Redux/Vuex的store
工厂模式 对创建对象逻辑的封装 jQuery的$(selector)
观察者模式 当一个对象被修改时,会自动通知它的依赖对象 Redux的subscribe、Vue的双向绑定
装饰器模式 对类的包装,动态地拓展类的功能 React高阶组件、ES7 装饰器
适配器模式 兼容新旧接口,对类的包装 封装旧API
代理模式 控制对象的访问 事件代理、ES6的Proxy(vue3的响应式原理实现)

# 单一职责原则和开放封闭原则

  • 单一职责原则:一个类只负责一个功能领域中的相应原则。或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
  • 开放封闭原则:核心的思想是软件实体(类、模块、函数等)是可扩展的、但不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。

# 单例模式

即一个类只能构造一个唯一的实例,其意义在于共享,唯一,Redux/Vuex中的store、JQ的$或者业务场景中的购物车、登录框都是单例模式的应用

class SingletonLogin {
  constructor(name,password){
    this.name = name
    this.password = password
  }
  static getInstance(name,password){
    //判断对象是否已经被创建,若创建则返回旧对象
    if(!this.instance)this.instance = new SingletonLogin(name,password)
    return this.instance
  }
}
 
let obj1 = SingletonLogin.getInstance('CXK','123')
let obj2 = SingletonLogin.getInstance('CXK','321')
 
console.log(obj1===obj2)    // true
console.log(obj1)           // {name:CXK,password:123}
console.log(obj2)           // 输出的依然是{name:CXK,password:123}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

代码核心思想是如果执行过了就返回上次的执行结果,这样就保证了多次调用也会拿到一样的结果。

# 优点

  • 划分命名空间,减少全局变量
  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
  • 只会实例化一次。简化了代码的调试和维护

# 不足

  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试。

# 工厂模式

工厂模式即对创建对象逻辑的封装,或者可以简单理解为对new的封装,这种封装就像创建对象的工厂,故名工厂模式。一个简单的工厂模式代码如下:

function factory(type) {
  switch(type) {
    case 'type1':
      return new Type1();
    case 'type2':
      return new Type2();
    case 'type3':
      return new Type3();
  }
}
1
2
3
4
5
6
7
8
9
10

上述代码中,我们传入了type,然后工厂根据不同的type来创建不同的对象。

工厂模式常见于大型项目,比如JQ的$对象,我们创建选择器对象时之所以没有new selector就是因为$()已经是一个工厂方法,其他例子例如 React.createElement()、Vue.component() 都是工厂模式的实现。

# 优点

  • 创建对象的过程可能很复杂,但我们只需要关心创建结果
  • 一个调用者想创建一个对象,只要知道其名称就可以了。
  • 构造函数和创建者分离, 符合“开闭原则”
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。

# 不足

  • 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
  • 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度

# 观察者模式

观察者模式在前端中很常见,观察者模式概念很简单:观察者监听被观察者的变化,被观察者发生改变时,通知所有的观察者。

观察者模式定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己,当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。观察者模式被广泛用于监听事件的实现,常用场景如Vue的双向绑定

//观察者
class Observer {    
  constructor (fn) {      
    this.update = fn    
  }
}
//被观察者
class Subject {    
    constructor() {        
        this.observers = []          //观察者队列    
    }    
    addObserver(observer) {          
        this.observers.push(observer)//往观察者队列添加观察者    
    }    
    notify() {                       //通知所有观察者,实际上是把观察者的update()都执行了一遍       
        this.observers.forEach(observer => {        
            observer.update()            //依次取出观察者,并执行观察者的update方法        
        })    
    }
}

var subject = new Subject()       //被观察者
const update = () => {console.log('被观察者发出通知')}  //收到广播时要执行的方法
var ob1 = new Observer(update)    //观察者1
var ob2 = new Observer(update)    //观察者2
subject.addObserver(ob1)          //观察者1订阅subject的通知
subject.addObserver(ob2)          //观察者2订阅subject的通知
subject.notify()                  //发出广播,执行所有观察者的update方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

虽然很多人把观察者模式又称为发布订阅模式,其实二者是有所区别的,发布订阅相较于观察者模式多一个调度中心。

# 优点

  • 增加了灵活性,支持简单的广播通信,自动通知所有已经订阅过的对象
  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
  • 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。

# 不足

  • 过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解

# 装饰器模式

在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法,注解也可以理解为装饰器。例如我有一些老代码,但是这些老代码功能不够,需要添加功能,但是我又不能去改老代码,比如Vue 2.x需要监听数组的改变,给他添加响应式,但是他又不能直接修改Array.prototype。这种情况下,就特别适合使用装饰者模式,给老方法重新装饰下,变成一个新方法来使用。

常见应用如:ES7的装饰器语法以及React中的高阶组件(HoC)都是这一模式的实现,Vue数组的监听(Object.defineProperty 这个方法不能监听数组的改变,通过改写数组方法实现),react-redux的connect()也运用了装饰器模式

这里以ES7的装饰器为例:

function info(target) {
  target.prototype.name = '张三'
  target.prototype.age = 10
}

@info
class Man {}

let man = new Man()
man.name // 张三
1
2
3
4
5
6
7
8
9
10

# 优点

  • 装饰类和被装饰类都只关心自身的核心业务,实现了解耦。
  • 方便动态的扩展功能,且提供了比继承更多的灵活性。

# 不足

  • 多层装饰比较复杂
  • 常常会引入许多小对象,看起来比较相似,实际功能大相径庭,从而使得我们的应用程序架构变得复杂起来

# 适配器模式

适配器模式,将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作。当我们面临接口不通用,接口参数不匹配等情况,我们可以在他外面再包一个方法,这个方法接收我们现在的名字和参数,里面调用老方法传入以前的参数形式。常用场景:整合第三方SDK,封装旧接口

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充电头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 优点

  • 可以让任何两个没有关联的类一起运行
  • 适配对象,适配库,适配数据
  • 提高类的复用

# 不足

  • 额外对象的创建,非直接调用,存在一定的开销(且不像代理模式在某些功能点上可实现性能优化)

# 代理模式

是为一个对象提供一个代用品或占位符,以便控制对它的访问。为一个对象找一个替代对象,以便对原对象进行访问。即在访问者与目标对象之间加一层代理,通过代理做授权和控制。最常见的例子是经纪人代理明星业务,假设你作为一个投资者,想联系明星打广告,那么你就需要先经过代理经纪人,经纪人对你的资质进行考察,并通知你明星排期,替明星本人过滤不必要的信息。事件代理、节流防抖函数、JQuery的$.proxy、ES6的proxy都是这一模式的实现

以ES6的proxy为例:

const idol = {
  name: '蔡x抻',
  phone: 10086,
  price: 1000000  //报价
}

const agent = new Proxy(idol, {
  get: function(target) {
    //拦截明星电话的请求,只提供经纪人电话
    return '经纪人电话:10010'
  },
  set: function(target, key, value) {
    if(key === 'price' ) {
      //经纪人过滤资质
      if(value < target.price) throw new Error('报价过低')
      target.price = value
    }
  }
})


agent.phone        //经纪人电话:10010
agent.price = 100  //Uncaught Error: 报价过低
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 优点

  • 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
  • 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;

# 不足

  • 处理请求速度可能有差别,非直接访问存在开销
装饰者模式实现上和代理模式类似,但是其不同点在于:
- 装饰者模式: 扩展功能,原有功能不变且可直接使用
- 代理模式: 显示原有功能,但是经过限制之后的

参考:(前端基础拾遗)[https://juejin.im/post/5e8b261ae51d4546c0382ab4#heading-63]、(如何封装代码)[https://juejin.im/post/5ec737b36fb9a04799583002#heading-3]、(从js中看设计模式)[https://juejin.im/post/5e4a87776fb9a07ca714ae54]
1
2
3
4
Last Updated: 4/16/2023, 11:00:44 PM