javascript设计模式学习

602 阅读16分钟

设计模式

一直知道设计模式这四个字,但是从来没有学习过设计模式,不明白设计模式是什么,有哪些,有什么作用,什么时候来使用,为什么要使用。这次借着分享的机会,对设计模式进行了一次全面的学习。终于揭开了我心中,设计模式的神秘面纱

什么是模式?

在学习设计模式前,我们先来一起了解一下设计模式是什么

设计模式(Design pattern) 是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。我们使用设计模式最终的目的是实现代码的 高内聚 和 低耦合。

什么是高内聚和低耦合?

举例一个现实生活中的例子,例如一个公司,一般都是各个部门各司其职,互不干涉。各个部门需要沟通时通过专门的负责人进行对接。在软件里面也是一样的 一个功能模块只是关注一个功能,一个模块最好只实现一个功能。这个是所谓的内聚,模块与模块之间、系统与系统之间的交互,是不可避免的, 但是我们要尽量减少由于交互引起的单个模块无法独立使用或者无法移植的情况发生, 尽可能多的单独提供接口用于对外操作, 这个就是所谓的低耦合

设计模式遵循solid原则

  • 单一职责原则 一个类应该只有一个发生变化的原因
  • 开放封闭原则 一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭
  • 里氏代换原则 所有引用基类的地方必须能透明地使用其子类的对象
  • 迪米特法则 只与你的直接朋友交谈,不跟“陌生人”说话
  • 接口隔离原则 客户端不应该依赖它不需要的接口、类间的依赖关系应该建立在最小的接口上
  • 依赖倒置原则 上层模块不应该依赖底层模块,它们都应该依赖于抽象、抽象不应该依赖于细节,细节应该依赖于抽象

使用模式的好处

  • 复用模式有助于方式在应用程序开发过程中的小问题引发的大问题
  • 模式可以提供通用的解决方案,并且其记录方式不需要与某个特定问题挂钩
  • 某些模式确实能够通过避免代码复用来减少代码的总体资源占用量
  • 模式添加到开发人员的词汇中,会使得沟通更快速
  • 经常使用的模式可以逐步该井,因为其他开发人员使用这些模式后总结出的共同的经验又贡献到设计模式社区

在学习设计模式的过程中发现,其实我们每天都在使用设计模式解决问题。因此掌握各种设计模式可以更好的利用设计模式编写出,可复用,高内聚,低耦合的代码。也可以便利与我们程序员之间的相互沟通。

设计模式的类别

  • 创建型设计模式

创建模式专注于处理对象创建机制,在学习创建模式之前,一定要对类的继承有清楚的了解认知。

创建模式包含: 工厂模式、建造者模式、原型模式、单例模式

  • 结构型设计模式

结构型设计模式:外观模式、适配器模式、代理模式、装饰者模式、桥接模式、组合模式、享元模式

  • 行为型设计模式

行为型模式:模板方法模式、观察者模式、状态模式、策略模式、职责链模式、命令模式、访问者模式、终结者模式、备忘录模式、迭代器模式、解释器模式、中介者模式

  • 技巧型设计模式

技巧型设计模式是通过一些特定技巧来解决组件的某些方面的问题,这类技巧一般通过实践经验总结得到。

技巧型设计模式:链模式、委托模式、数据访问对象模式、节流模式、简单模板模式、惰性模板、参与者模式、等待者模式。

  • 架构型式设计模式

架构型设计模式是一类框架结构,通过提供一些子系统,指定他们的职责。并将它们调理清晰的组织在一起

架构型式设计模式: 同步模块模式、异步模块模式、widget模式、MVC模式、MVP模式、MVVM模式

常见设计模式

工厂(Factory)模式

公司像一个大工厂,当客户需要公司的某项服务,比如寄快递。只要和快递员沟通。下单。并不关心,快递怎么运输到指定地点。快递的运输。派送。交给公司去解决。

Factory :工厂,负责返回产品实例 Product :产品,访问者从工厂拿到产品实例 工厂模式

/* 工厂类 */
class Factory {
    static getInstance(type) {
        switch (type) {
            case 'Product1':
                return new Product1()
            case 'Product2':
                return new Product2()
            default:
                throw new Error('当前没有这个产品')
        }
    }
}

/* 产品类1 */
class Product1 {
    constructor() { this.type = 'Product1' }
    
    operate() { console.log(this.type) }
}

/* 产品类2 */
class Product2 {
    constructor() { this.type = 'Product2' }
    
    operate() { console.log(this.type) }
}

const prod1 = Factory.getInstance('Product1')
prod1.operate()	
const prod2 = Factory.getInstance('Product3')

工厂模式优点

工厂模式将对象的创建和实现分离,良好的封装,代码结构清晰,访问者无需知道对象的创建流程,特别是创建比较复杂的情况下 扩展性优良,通过工厂方法隔离了用户和创建流程隔离,符合开放封闭原则 解耦了高层逻辑和底层产品类,符合最少知识原则,不需要的就不要去交流

工厂模式的缺点

带来了额外的系统复杂度,增加了抽象性

单例(Singleton)模式

单例模式可能是设计模式里面最简单的模式了,虽然简单,但在我们日常生活和编程中却经常接触到

单例模式,在该实例不存在的情况下,可以通过一个方法创建一个类来实现创建类的新实力,如果实例已经存在,他会返回该对象的引用。

单例模式主要解决的问题就是节约资源,保持访问一致性。

class SingleObject{}

SingleObject.getInstance = (function(){
    let instance
    return function(){
        if(!instance){
            instance = new SingleObject()
        }
        return instance
    }
})()

let obj1 = SingleObject.getInstance()
let obj2 = SingleObject.getInstance()
console.log(obj1===obj2)

单例模式的优缺点

单例模式的优点

单例模式在创建后在内存中只存在一个实例,节约了内存开支和实例化时的性能开支,特别是需要重复使用一个创建开销比较大的类时,比起实例不断地销毁和重新实例化,单例能节约更多资源,比如数据库连接 单例模式可以解决对资源的多重占用,比如写文件操作时,因为只有一个实例,可以避免对一个文件进行同时操作 只使用一个实例,也可以减小垃圾回收机制 GC(Garbage Collecation) 的压力,表现在浏览器中就是系统卡顿减少,操作更流畅,CPU 资源占用更少

单例模式的优点

单例模式对扩展不友好,一般不容易扩展,因为单例模式一般自行实例化,没有接口; 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化;

单例模式的使用场景

那我们应该在什么场景下使用单例模式呢

当一个类的实例化过程消耗的资源过多,可以使用单例模式来避免性能浪费 当项目中需要一个公共的状态,那么需要使用单例模式来保证访问一致性

外观(Facade)模式

外观模式 (Facade Pattern)又叫门面模式,定义一个将子系统的一组接口集成在一起的高层接口,以提供一个一致的外观。外观模式让外界减少与子系统内多个模块的直接交互,从而减少耦合,让外界可以更轻松地使用子系统。本质是封装交互,简化调用。

外观模式在源码中的使用很多,比如vue createElement方法、Lodash createRange、Axios

url

var preventDefault = function(e){
    var event = e||window.event
    if(event.preventDefault){
        event.preventDefault()
    }else{
        event.returnValue = false
    }
}

外观模式的优点

访问者不需要再了解子系统内部模块的功能,而只需和外观交互即可,使得访问者对子系统的使用变得简单,符合最少知识原则,增强了可移植性和可读性 减少了与子系统模块的直接引用,实现了访问者与子系统中模块之间的松耦合,增加了可维护性和可扩展性 通过合理使用外观模式,可以帮助我们更好地划分系统访问层次,比如把需要暴露给外部的功能集中到外观中,这样既方便访问者使用,也很好地隐藏了内部的细节,提升了安全性

外观模式的缺点

不符合开闭原则,对修改关闭,对扩展开放,如果外观模块出错,那么只能通过修改的方式来解决问题,因为外观模块是子系统的唯一出口 不需要或不合理的使用外观会让人迷惑,过犹不及

外观模式的适用场景

维护设计粗糙和难以理解的遗留系统,或者系统非常复杂的时候,可以为这些系统设置外观模块,给外界提供清晰的接口,以后新系统只需与外观交互即可 你写了若干小模块,可以完成某个大功能,但日后常用的是大功能,可以使用外观来提供大功能,因为外界也不需要了解小模块的功能 团队协作时,可以给各自负责的模块建立合适的外观,以简化使用,节约沟通时间 如果构建多层系统,可以使用外观模式来将系统分层,让外观模块成为每层的入口,简化层间调用,松散层间耦合

发布订阅模式

发布订阅模式也是观察者模式,是js中为常见的一种模式。

Publisher :发布者,当消息发生时负责通知对应订阅者 Subscriber :订阅者,当消息发生时被通知的对象 SubscriberMap :持有不同 type 的数组,存储有所有订阅者的数组 type :消息类型,订阅者可以订阅的不同消息类型 subscribe :该方法为将订阅者添加到 SubscriberMap 中对应的数组中 unSubscribe :该方法为在 SubscriberMap 中删除订阅者 notify :该方法遍历通知 SubscriberMap 中对应 type 的每个订阅者

url

class Publisher {
    constructor() {
        this._subsMap = {}
    }
    
    /* 消息订阅 */
    subscribe(type, cb) {
        if (this._subsMap[type]) {
            if (!this._subsMap[type].includes(cb))
                this._subsMap[type].push(cb)
        } else this._subsMap[type] = [cb]
    }
    
    /* 消息退订 */
    unsubscribe(type, cb) {
        if (!this._subsMap[type] ||
            !this._subsMap[type].includes(cb)) return
        const idx = this._subsMap[type].indexOf(cb)
        this._subsMap[type].splice(idx, 1)
    }
    
    /* 消息发布 */
    notify(type, ...payload) {
        if (!this._subsMap[type]) return
        this._subsMap[type].forEach(cb => cb(...payload))
    }
}

const shop = new Publisher()

shop.subscribe('运动鞋', message => console.log('152xxx' + message))    // 订阅运动鞋
shop.subscribe('运动鞋', message => console.log('138yyy' + message))
shop.subscribe('帆布鞋', message => console.log('139zzz' + message))    // 订阅帆布鞋

shop.notify('运动鞋', ' 运动鞋到货了 ~')   // 打电话通知买家运动鞋消息
shop.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息

// 输出:  152xxx 运动鞋到货了 ~
// 输出:  138yyy 运动鞋到货了 ~
// 输出:  139zzz 帆布鞋售罄了 T.T

发布-订阅模式的应用

  • vue 中的EventBus

  • 发布-订阅模式在源码中应用很多,特别是现在很多前端框架都会有的双向绑定机制的场景.

vue双向绑定原理图

响应式化后的数据相当于发布者。

每个组件都对应一个 Watcher 订阅者。当每个组件的渲染函数被执行时,都会将本组件的 Watcher 放到自己所依赖的响应式数据的订阅者列表里,这就相当于完成了订阅,一般这个过程被称为依赖收集(Dependency Collect)。 组件渲染函数执行的结果是生成虚拟 DOM 树(Virtual DOM Tree),这个树生成后将被映射为浏览器上的真实的 DOM 树,也就是用户所看到的页面视图。

当响应式数据发生变化的时候,也就是触发了 setter 时,setter 会负责通知(Notify)该数据的订阅者列表里的 Watcher,Watcher 会触发组件重渲染(Trigger re-render)来更新(update)视图。

发布-订阅模式的优点

时间上的解耦 :注册的订阅行为由消息的发布方来决定何时调用,订阅者不用持续关注,当消息发生时发布者会负责通知 对象上的解耦 :发布者不用提前知道消息的接受者是谁,发布者只需要遍历处理所有订阅该消息类型的订阅者发送消息即可(迭代器模式),由此解耦了发布者和订阅者之间的联系,互不持有,都依赖于抽象,不再依赖于具体 由于它的解耦特性,发布-订阅模式的使用场景一般是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变。发布-订阅模式还可以帮助实现一些其他的模式,比如中介者模式。

发布-订阅模式缺点

  • 增加消耗 :创建结构和缓存订阅者这两个过程需要消耗计算和内存资源,即使订阅后始终没有触发,订阅者也会始终存在于内存
  • 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的那堆 deps/subs/watchers 们… 缺点主要在于理解成本、运行效率、资源消耗,特别是在多级发布-订阅时,情况会变得更复杂。

观察者(Observer)模式和发布-订阅(Publish/Subscribe)的区别

区别

区别主要在发布-订阅模式中间的这个 Event Channel:

观察者模式 中的观察者和被观察者之间还存在耦合,被观察者还是知道观察者的; 发布-订阅模式 中的发布者和订阅者不需要知道对方的存在,他们通过消息代理来进行通信,解耦更加彻底

装饰者(Decorator) 模式

又称装饰器模式,在不改变原对象的基础上,通过对其添加属性或方法来进行包装拓展,使得原有对象可以动态具有更多功能。


function testDesc (target){
    target.isDesc = true
}

@testDesc
class Demo{
}

console.log(Demo.isDesc)

装饰者模式的优点

我们经常使用继承的方式来实现功能的扩展,但这样会给系统中带来很多的子类和复杂的继承关系,装饰者模式允许用户在不引起子类数量暴增的前提下动态地修饰对象,添加功能,装饰者和被装饰者之间松耦合,可维护性好; 被装饰者可以使用装饰者动态地增加和撤销功能,可以在运行时选择不同的装饰器,实现不同的功能,灵活性好; 装饰者模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,可以给一个对象增加多个同样的装饰器,也可以把一个装饰器用来装饰不同的对象,有利于装饰器功能的复用; 可以通过选择不同的装饰者的组合,创造不同行为和功能的结合体,原有对象的代码无须改变,就可以使得原有对象的功能变得更强大和更多样化,符合开闭原则;

装饰者模式的缺点

使用装饰者模式时会产生很多细粒度的装饰者对象,这些装饰者对象由于接口和功能的多样化导致系统复杂度增加,功能越复杂,需要的细粒度对象越多; 由于更大的灵活性,也就更容易出错,特别是对于多级装饰的场景,错误定位会更加繁琐;

装饰者模式的适用场景

如果不希望系统中增加很多子类,那么可以考虑使用装饰者模式; 需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,这时采用装饰者模式可以很好实现; 当对象的功能要求可以动态地添加,也可以动态地撤销,可以考虑使用装饰者模式;

委托模式(Entrust)

多个对象接收并处理同一请求,他们将请求委托给另一个对象统一处理请求。

典型:事件委托机制

委托模式是通过委托者将请求委托给被委托者去处理实现的。因此委托模式解决了请求与委托者之间的耦合。通过被委托者对接收到的请求的处理后,分发给相应的委托者去处理。在JavaScript中,委托模式已经得到很广泛的应用,尤其在处理事件上,当然委托模式是一种基础技巧,因此也同样在其他设计模式中被引用,如状态模式中状态对象对接收的状态处理,策略模式中策略对象对接收到的算法处理,命令模式中命令对象对接收到的命令处理等。

委托模式的优点

委托模式优化了页面中的事件绑定。对于未来元素的事件绑定是现有技术所做不到的。也有防止内存外泄的问题