“浅尝”JavaScript设计模式

5,787 阅读12分钟

什么是设计模式?

设计模式:根据不同场景创建不同类型的对象的套路被称为设计模式。

使用设计模式的主要原因?

①可维护性:设计模式有助于降低模块间的耦合程度,这使对代码进行重构和换用不同的模块变得更容易,也使得程序员在大型团队中的工作以及与其他程序员的合作变得更容易。
②沟通:设计模式为处理不同类型的对象提供了一套通用的术语,程序员可以更简明地描述自己的系统的工作方式,你不用进行冗长的说明,往往一句话,我是用了什么设计模式,每个模式有自己的名称,这意味着你可以在较高层面上进行讨论,而不必涉足过多的细节
③性能:某些模式是起优化作用的模式,可以大幅度提高程序的运行速度,并减少需要传送到客户端的代码量

设计模式

实现设计模式比较容,懂得应该在什么时候使用什么模式比较困难,未搞懂设计模式的用途就盲目套用,是一种不安全的做法,你应该保证所选用的模式就是最恰当的那种,并且不要过度牺牲性能。

一、单例模式

确保单体对象只存在一个实例。

业务场景:当我们使用node启动一个服务连接数据库的时候我们一般会创建一个连接数据库的实例(这个实例就是单例)。每个请求对于数据的请求都是通过这个单例的,不会为没个请求去创建单独的实例,一个单例便于统一管理。

var Single = (function () {
  var instance
  var createSingle = function (name) {
    if (instance) {
      return instance
    }
    this.name = name
    instance = this
    return instance
  }
  return createSingle
})();

var a = new Single('123')
var b = new Single('456')
console.log(a === b) // true

二、工厂模式

工厂模式使用一个方法来决定究竟要实例化哪个具体的类。

  • 简单工厂模式

使用一个总的工厂来完成对于所有类的分发。情景再现:一个宠物店里面有着许多宠物,客人可以通过向宠物店传递消息 于是第二天我们就可以到宠物店获取一只猫
①工厂(宠物店) ②传参(传递消息) ③实例化对应类(猫)

class Cat{
   constructor() {
       this.name = '猫'
   }
}
class Dog{
   constructor() {
       this.name = '狗'
   }
}
class Factory {
    constructor(role){
      return this.switchRole(role)
    }
    switchRole(role){
      switch(role){
        case '猫':
            return new Cat()
        case '狗':
            return new Dog()
        default:
            return {}
      }
    }
}
var dog = new Factory('狗') // {name:'猫'}
var cat = new Factory('猫') // {name:'狗'}

简单的工厂模式我们已经实现了,这时候我们需要又要发散我们的小脑袋去好好揣摩这个模式,我们发现如果每次宠物店又有了新的宠物可以出售,例如今天宠物店引进了乌龟、宠物猪,那我们不仅要先实现相对应的类,还要在Factory中的switchRole()补充条件。如果创建实例的方法的逻辑发生变化,工厂的类就会被多次修改

  • 复杂工厂模式

既然简单工厂模式,不能满足我们全部的业务需求,那就只能进化变身了。《javascript设计模式》 给了定义:真正的工厂模式与简单工厂模式区别在于,它不是另外使用一个类或对象创建实例,而是使用子类工厂是一个将其成员对象的实例推送到子类中进行的类 也就是我们在定义我们看到真正的工厂模式,是提供一个工厂的父类抽象类,对于根据传参实现实例化的过程是放在子类中实现。

再思考一个问题:猫、狗、乌龟、宠物猪、这些类是否可以再进行细分出现了加菲猫、波斯猫、柴犬、阿拉斯加等子类。在购买宠物的时候是否需要特别的条件? 上述工厂的子类可以解决这个问题:出现了专卖店专门卖猫的,卖狗的,卖宠物猪的,这些专卖店(工厂子类)各自维护自己的类,当你需要新的专卖店你可以重新实现一个工厂子类

class Cat{
   constructor() {
       this.name = '猫'
   }
}
class Garfield {
   constructor() {
       this.name = '加菲猫'
   }
}
class Persian {
   constructor() {
       this.name = '波斯猫'
   }
}
class Dog{
   constructor() {
       this.name = '狗'
   }
}
// 定义成为抽象类,工厂的父类,不接受任何修改
class Factory {
    constructor(role){
      return this.createModule(role)
    }
    createModule(role){
        return new Error('我是抽象类不要改我,也不要是实例化我')
    }
}
// 猫的专卖工厂,需要重写父类那里继承来的返回实例的方法。购买猫的逻辑可以放在找个类中实现。
class CatFactory extends Factory{
    constructor(role){
      super(role)
    }
    // 重写createFactory的方法
    createModule(role){
        switch(role){
            case '加菲猫':
                return new Garfield()
            case '波斯猫':
                return new Persian()
            default:
                return {}
          }
    }
}
.... 狗、宠物猪、乌龟的都可以重新继承父类Factory。
var catFac = new CatFactory('波斯猫')
console.log(catFac)

总结:复杂工厂模式将原有的简单工厂模式下的工厂类变为抽象类根据输出实例的不同来构建不同的工厂子类。这样既不会修改到工厂抽象类,符合设计原则,又提供了可拓展性。

三、桥接模式

将抽象与其实现隔离开来,以便二者独立变化。

大家看到上面的那句话会觉得有点摸不清头脑看一下下面的代码:

//一个事件的监听,点击元素获得id,根据获得的id我们发送请求查询对应id的猫
element.addEventListener('click',getCatById)
var getCatById = function(e){
   var id = this.id
   asyncRequst('Get',`cat.url?id=${id}`,function(resp){
       console.log('我已经获取了信息')
   })
}

大家看一下getCatById这个api函数我们可以理解为抽象 而点击这个过程是实现的效果,但是我们发现getCatById与实现逻辑并没有完全分割,getCatById是一个只能工作在浏览器中的API,根据事件监听器函数的工作机制,事件对象自然会被作为第一个参数传递给这个函数,在本例中并没有使用,但是我们看到了var id = this.id,如果你要对这个API做单元测试,或者命令环境中执行,那就只能祝你好运了,任何一个API都不应该把它与任何特定环境搅在一起

// 改写getCatById 将抽象与现实完全隔离,抽象完全依赖传参,同时我们在别的地方也可以引用,不受制与业务
var getCatById = function(id,callback){
   asyncRequst('Get',`cat.url?id=${id}`,function(resp){
       console.log('我已经获取了信息')
   })
}

这个时候我们发现了现在抽象出来的getCatById已经不能直接作为事件函数的回调了,这时候我们要隆重的请出我们桥接模式 此处应该有撒花。

element.addEventListener('click',getCatByIdBridge)
var getCatByIdBridge(e){ // getCatByIdBridge 桥接元素
    getCatById(this.id,function(cat){
        console.log('request cat')
    })
}

我们可以看到getCatByIdBridge这个就是桥接模式的产物。沟通抽象与现实的桥梁。有了这层桥接,getCatById这个API的使用范围就大大的拓宽了,没有与事件对象捆绑在了一起。

总结:在实现API的时候,桥接模式非常有用,我们用这个模式来弱化API与使用他的类和对象之间的耦合,这种模式对于js中常见的事件驱动有大的裨益。

四、策略模式

定义一系列的规则,根据环境的不同我们执行不同的规则,来避免大量重复的工作。

策略模式根据上述的说法可以隐隐的猜到了,要实行策略模式我们需要两个类:首先我们需要一个定义了一系列规则的策略类这个是整个策略模式的基石。之后有一个暴露对外方法的环境类通过传递不同的参数给环境类,环境类从策类中选取不同的方法执行,最终返回结果

业务场景:作为一个前端难免跑不了一个表单验证。当你不使用库的时候难免需要手写一些正则校验、判空、判类型等方式。 这个时候你的代码就是如下的:

// vue下的表单校验
checkForm () {
  if (this.form.realName === '') {
    Toast.fail('真实姓名不能为空')
    return false
  }
  if (!/^[\u4e00-\u9fa5]{0,}$/.test(this.form.realName)) {
    Toast.fail('请输入中文')
    return false
  }
  if (!/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(this.form.idCardNum)) {
    Toast.fail('请输入正确的身份证格式')
    return false
  }
  return true
},

注意:这样写是没有问题的,但是我们仔细看这个代码会发现,这个代码几乎不可复用,即使在其他的地方我们需要对字段判定中文,我们就需要重新再用正则判定,其次当一个表单内部需要校验字段很多,那么上述方法将会过于冗余

修改思路:将校验的具体的实现我们放入策略类,这样对于可重复使用检验规则我们使用一个单个方法统一维护,之后将暴露环境类给各个业务场景使用,传入所需的方法名、检测值然后调用策略返回结果。

// 策略类为表单校验正则及及自定义的规则
var rules = {
  // 是否中文
  isChinese: function (value) {
    if (/^[\u4e00-\u9fa5]{0,}$/.test(value)) {
      return true
    } else {
      return false
    }
  },
  //  是否不为空
  notNull: function (value) {
    if (value !== '') {
      return true
    } else {
      return false
    }
  },
.... // 不同策略
}


// 环境类
var validate = function(rule,value) {
    return rules[rule](value);
};

//业务执行
const isChinese = validate('isChinese',value)
const notNull = validate('notNull',value)
const checkResult = isChinese||notNull
if(checkResult){
    .....
}

总结:一个简单的策略模式就已经实现了,将校验的抽象方法放在策略类,然后根据参数的不同去调用不同的策略,实现了策略的复用,也实现了代码的简化。但是这样的策略是你心中真正的爱么兄弟?

缺点:上述的代码已经满足了策略模式,但是对于具体业务的支持似乎还有点小瑕疵,首先上述的业务执行,你会发现对于单个校验我们返回的是boolean值,如果我们需要有失败的回调函数的调用,那就还是需要判断语句的加入,真的是鱼和熊掌不可兼得,也就是说上述的代码,只能支持表单项全部检测完成后总的失败回调,对于单个表单项的失败无法支持,用户往往输入完成全部表单项后才被告知表单中有错误,而且还不知道具体是哪个。

优化:修改环境类,参数对象形成的数组或单个参数对象传入,参数对象传入时提供失败函数,对外提供一个检验方法,遍历检查参数对象数组,全部成功返回true,失败执行失败回调函数同时返回false。

class Validate {
  constructor () {
    this.cache = []
    if (Array.isArray(arguments[0])) {
      // 数组的单个元素{rule:string[规则的名称],value:any[校验的值],efn:fn[失败的回调]}
      this.cache = arguments[0]
    }
    // 传入参数为对象时
    this.cache.push(arguments[0])
  }
  // 执行校验,失败的话执行失败的回调,成功静默,所有的参数符合规则则返回true
  valid () {
    let i = 0
    for (const value of this.cache) {
      if (rules[value.rule] && rules[value.rule](value.value)) {
        i++
      } else {
        if (value.efn) value.efn()
        return false
      }
    }
    return i === this.cache.length
  }
}

总结:策略模式可以应用使用判断语句多的时候,判断内容为同种模式的情况下。策略模式中的策略类可以进行复用,从而避免很多地方的复制粘贴,独立的策略类易于理解、切换、拓展。

五、中介者模式

中介者模式的作用就是解除对象与对象之间的紧耦合关系。对象与对象中的通信以中介者为媒介触发。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。

场景分析:设定两个角色房东、租客。房东与租客组成的是多对多的网格关系,一个房东可以与多名租客接触,询问是否有租房意愿,同理租客可以向多名房东打探是否有房源出售。

前置条件我们实例化两个角色房东、租客,我们使用简单工厂模式。

// 房东类
class Owener{
    constructor(name){
        this.name=name
    }
    // 想要出租
    sell(renters=[]){
        renters.map(renter=>{
            renter.onMessage(`${this.name}想要出租房子`)
        })
    }
    // 收到消息
    onMessage(msg){
        console.log(`${this.name}知道了${msg}`)
    }
}
// 租客类
class Renter{
    constructor(name){
        this.name=name
    }
    // 收到消息
    onMessage(msg){
        console.log(`${this.name}知道了${msg}`)
    }
}
class Factory{
    constructor(role,name){  
        this.name = name
        return this.switchRole(role,name)
    }
    switchRole(role,name){
        switch (role) {
            case 'owner':
                return new Owener(name)
            case 'renter':
                return new Renter(name)
            default:
                return new Error('无当前角色')
        }
    }
}
var owner1 = new Factory('owner','房东一')
var owner2 = new Factory('owner','房东二')
var renter1 = new Factory('renter','租客一')
var renter2 = new Factory('renter','租客二')
var renter3 = new Factory('renter','租客三')
owner.sell([renter1,renter2,renter3])

上述代码完成了房东一 发布出租房子的意愿,三名租客接受到了消息。反向同理也可以实现。租客发布消息,房东收到,但是我们发现房东 与租客之间。还是存在紧密的耦合,房东与租客之间不同的关系需要自行不同的方法,那整个房东类会变得及其臃肿,反之亦然。于是我们引入中介者模式

中介者模式在上述例子中理解为中介公司,扮演了维护房东 和租客关系的桥梁,租客和房东类只要考虑各自的行为,不需要考虑行为会给那些关系对象带来影响。这些任务交给我们的 中介者

优化:生成一个中介者来处理房东与租客的相互调用,以及确定相互关系,中介者提供一个双向的方法,供房东和租客调用,然后实现对相关对象的分发。

class Owener{
    constructor(name){
        this.name=name
    }
    // 想要出租
    sell(mediator){
        mediator.sendMessage('owner',`${this.name}想要出租房子`)
    }
    // 收到消息
    onMessage(msg){
        console.log(`${this.name}知道了${msg}`)
    }
}
class Renter{
    constructor(name){
        this.name=name
    }
    // 想要租房
    rent(mediator){
        mediator.sendMessage('renter',`${this.name}想要租房子`)
    }
    // 收到消息
    onMessage(msg){
        console.log(`${this.name}知道了${msg}`)
    }
}
class Factory{
    constructor(role,name){  
        this.name = name
        return this.switchRole(role,name)
    }
    switchRole(role,name){
        switch (role) {
            case 'owner':
                return new Owener(name)
                break;
            case 'renter':
                return new Renter(name)
                break;
            default:
                return new Error('无当前角色')
                break;
        }
    }
}
class Mediator{
    constructor(owner,renter){
      // 房东集合
      this.owner = owner
      // 房客集合
      this.renter = renter
    }
    sendMessage(role,msg){
       if(role === 'owner'){
           for(const value of this.renter){
               value.onMessage(msg)
           }
       }
       if(role === 'renter'){
           for(const value of this.owner){
               value.onMessage(msg)
           }
       }
    }
}
var owner1 = new Factory('owner','房东一')
var owner2 = new Factory('owner','房东二')
var renter1 = new Factory('renter','租客一')
var renter2 = new Factory('renter','租客二')
var renter3 = new Factory('renter','租客三')
var mediator = new Mediator([owner1,owner2],[renter1,renter2,renter3])
owner1.sell(mediator)
租客一知道了房东一想要出租房子
租客二知道了房东一想要出租房子
租客三知道了房东一想要出租房子
renter1.rent(mediator)
房东一知道了租客一想要租房子
房东二知道了租客一想要租房子

总结:房东、租客各自维护自己的行为,通知调用中介者。中介者维护对象关系以及对象方法的调用。优点:对象的关系在中介者中可以自由定义、一目了然。减少了两个类的臃肿。去除了两个对象之间的紧耦合。缺点:两个关系类的不臃肿换来了中介者类的臃肿。

六、装饰者模式

为对象增添特性的技术,它并不使用创建新子类这种手段

场景分析:看了别的文章都是自行车的场景那我也来呗,嘻嘻~,自行车商店:出售A、B、C、D四种牌子的自行车,这时候可以选择配件车灯、前置篮。如果按照正常创造子类的方法,我们首先定义 A、B、C、D四个牌子的自行车父类,然后再根据配件通过继承父类来实现子类。

// A自行车父类
class Bicycle{
    constructor(){
        this.type = 'A'
    }
    getBicycle(){
        console.log('A自行车的售价100')
    }
}
// 拥有车灯的A类自行车类
class BicycleDeng extends Bicycle{
    constructor(){
        super()
    }
    setDeng(){
        console.log('我安装上了大灯')
    }
}

上述代码我们可以发现:当我们需要穷尽场景中可能出现的自行车的时候:A类有车灯、A类有前置栏、B类有车灯....一共要4*2一共八个类。这个时候我们使用的是创建新子类的手段将所有情况穷举到实体类上面,想要实例化一个对象只要找到对应实体类就好了。

优化:如果自行车的类型增多,配件增多就会出现实体类几何增加,你们应该不会想把自己的余生花在维护实体类上吧,来吧出来吧我们的装饰者模式。首先回归业务场景:A、B、C、D我们可以暂时称为组件。而我们的配件就是装饰者。主要思路:替换创建新子类,每个装饰者维护单个装饰者类,将组件作为实例传入装饰者类中进行‘装饰’可以重写原有方法、也可以新增方法。 这样我们只要维护组件类加装饰者类 4+2一个6个类

class Bicycle{
    constructor(){
        this.type = 'A'
        // 自行车售价
        this.price = 5000
    }
    getBicycle(){
        console.log(`A自行车的售价${this.price}`)
    }
}
class BicycleDecorator{
    constructor(bicycle){
        // 传入的组件
        this.bicycle = bicycle
        // 大灯售价
        this.dengPrice = 200
    }
    // 新增方法
    openDeng(){
        console.log('我打开了灯')
    }
    // 重写方法
    getBicycle(){
        console.log(`A自行车的售价${this.bicycle.price + this.dengPrice}`)
    }
}
// 先创建类型自行车
var a = new Bicycle()
// 装饰后替换原有
a = new BicycleDecorator(a)
a.getBicycle() // A自行车的售价5200

总结:装饰者模式为对象增添特性提供了新的思路,去除了创建新的子类对应最小单位实体类,通过传递一个父类来进行对于父类增加新特性的方法是,保留了父类原有方法也具有延展性。真香~~

结束语

本文提供了部分的设计模式以及自己的理解,理解可能存在偏差欢迎讨论,本文参考的是《javascript设计模式》。之后如有设计模式的补充还会继续更新。感谢 现在有表情了,没错 囧rz 的指出错误