【八】设计模式

前言

本篇章主要参考博客:https://juejin.cn/post/7072175210874535967#heading-5,这篇博客已经写的很详细了,篇章过长就不一一了解了,针对部分比较容易理解以及部分比较重要的设计模式,做了一些解释。

面试回答

1.设计模式:设计模式更像是一种解决问题的方案,也就是开发过程中常见的套路,它有这么几个原则:比如单一职责,尽可能缩小代码所覆盖的功能;开放封闭原则,对扩展开发,对修改封闭;里氏置换原则,子类能够覆盖父类;接口独立原则,避免出现过多接口;依赖倒置原则,就是让使用方更专注于接口。众多设计模式中较为人知的两者模式则是订阅者发布者模式以及观察者模式,两者的区别在于,订阅者发布者模式比观察者模式多了事件中心,让事件中心来处理相应逻辑。也就是说观察者模式是直接进行交互的,而订阅者发布者模式是借助事件中心进行交互的。

知识点

经典的设计模式有 23 种,参考博客中基本都涉及了。设计模式是解决问题的一种思想,一种解决方案,和语言无关,更多应用于框架、工具类、UI库等公用类的方面,在实际开发的业务场景应用较少,原因是业务场景基本都是比较独立,很难剥离出来由一个公共类去统一套用处理,即使可以还不如直接开发写逻辑来的迅速。简单来说,设计模式就是写代码中常见的套路。实现设计模式比较容易,懂得应该在什么时候使用什么模式比较困难,未搞懂设计模式的用途就盲目套用,是一种不安全的做法,你应该保证所选用的模式就是最恰当的那种,并且不要过度牺牲性能。

设计模式的五大设计原则

  • 单一职责:一个程序只需要做好一件事。如果功能过于复杂就拆分开,保证每个部分的独立。
  • 开放封闭原则:对扩展开放,对修改封闭。增加需求时,扩展新代码,而不是修改源代码。
  • 里氏置换原则:子类能覆盖父类,父类能出现的地方子类也能出现。
  • 接口独立原则:保持接口的单一独立,避免出现“胖接口”。
  • 依赖倒置原则:面向接口编程,依赖于抽象而不依赖于具体。使用方只专注接口而不用关注具体类的实现。俗称“鸭子类型”

设计模式的三大类

创建型:工厂模式,抽象工厂模式,建造者模式,单例模式,原型模式

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

行为型:策略模式,模板方法模式,发布订阅模式,迭代器模式,职责链模式,命令模式,备忘录模式,状态模式,访问者模式,中介者模式,解释器模式。

1.工厂模式

工厂模式是用来创建对象的常见设计模式,在不暴露创建对象的具体逻辑,将逻辑进行封装,那么它就可以被称为工厂。

优点

  1. 调用者创建对象时只要知道其名称即可。
  2. 扩展性高,如果要新增一个产品,直接扩展一个工厂类即可。
  3. 隐藏产品的具体实现,只关心产品的接口。

缺陷

  • 每次增加一个产品时,都需要增加一个具体类,这无形增加了系统内存的压力和系统的复杂度,也增加了具体类的依赖

实例

场景:一个服装店(工厂)里面有着各种款式,客人可以通过向服装店传递消息(传参),第二天我们就可以到服装店获取一件羽绒服(实例化对象)。

//具体类
class DownJacket {
  production(){
    console.log('生产羽绒服')
  }
}
class Underwear{
  production(){
    console.log('生产内衣')
  }
}
class TShirt{
  production(){
    console.log('生产t恤')
  }
}
// 工厂类
class clothingFactory {
  constructor(){
    this.downJacket = DownJacket
    this.underwear = Underwear
    this.t_shirt = TShirt
  }
  getFactory(clothingType){
    const _production = new this[clothingType]
    return _production.production()
  }
}
const clothing = new clothingFactory()
clothing.getFactory('t_shirt')// 生产t恤
clothing.getFactory('underwear')// 生产内衣

简单的工厂模式我们已经实现了,但是我们可以发现每次有新的衣服类型,都需要先新写一个具体类,然后再修改工厂类。由此引出复杂工厂模式,复杂工厂模式与简单工厂模式区别在于,它不是另外使用一个类或对象创建实例,而是使用子类。工厂是一个将其成员对象的实例推送到子类中进行的类也就是我们在定义我们看到真正的工厂模式,是提供一个工厂的父类抽象类,相当于结构,这个结构包含一些基础方法,这些方法可以通过继承重写,对于根据传参实现实例化的过程是放在子类中实现。

class DownJacket {
  production(){
    console.log('生产羽绒服')
  }
}
class Underwear{
  production(){
    console.log('生产内衣')
  }
}
class TShirt{
  production(){
    console.log('生产t恤')
  }
}

// 定义成为抽象类,工厂的父类,不接受任何修改
class Factory {
    constructor(role){
      return this.createModule(role)
    }
    createModule(role){
        return new Error('我是抽象类不要改我,也不要是实例化我')
    }
}
// 工厂需要重写父类那里继承来的返回实例的方法。
class ClothFactory extends Factory{
    constructor(role){
      super(role)
    }
    // 重写createFactory的方法
    createModule(role){
        switch(role){
            case 'TShirt':
                return new TShirt()
            case 'Underwear':
                return new Underwear()
            default:
                return {}
          }
    }
}
//T恤、内衣、羽绒服的都可以重新继承父类Factory。
var TFac = new ClothFactory('TShirt')
console.log(TFac.production())

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

2.单例模式

单例模式的思路是:保证一个类只能被实例一次,每次获取的时候,如果该类已经创建过实例则直接返回该实例,否则创建一个实例保存并返回。单例模式的核心就是创建一个唯一的对象,而在javascript中创建一个唯一的对象太简单了,为了获取一个对象而去创建一个类有点多此一举。如const obj = {},obj就是独一无二的一个对象,在全局作用域的声明下,可以在任何地方对它访问,这就满足了单例模式的条件。

优点

  1. 内存中只有一个实例,减少了内存的开销。
  2. 避免了对资源多重的占用。
  3. 模块间通信
  4. 保证某个类的对象的唯一性
  5. 防止变量污染

缺陷

  • 违反了单一职责,一个类应该只关心内部逻辑,而不用去关心外部的实现

实例

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

class LoginFrame {
    static instance = null
    constructor(state){
        this.state = state
    }
    show(){
        if(this.state === 'show'){
            console.log('登录框已显示')
            return
        }
        this.state = 'show'
        console.log('登录框展示成功')
    }
    hide(){
        if(this.state === 'hide'){
            console.log('登录框已隐藏')
            return
        }
        this.state = 'hide'
        console.log('登录框隐藏成功')
    }
    // 通过静态方法获取静态属性instance上是否存在实例,如果没有创建一个并返回,反之直接返回已有的实例
    static getInstance(state){
        if(!this.instance){
            this.instance = new LoginFrame(state)
        }
        return this.instance
    }
}
const p1 = LoginFrame.getInstance('show')
const p2 = LoginFrame.getInstance('hide')
console.log(p1 === p2) // true

3.原型模式

原型模式是指原型实例指向创建对象的种类,通过拷贝这些原型来创建新的对象,说白了就是克隆自己,生成一个新的对象。

优点

  1. 不再依赖构造函数或者类创建对象,可以将这个对象作为一个模板生成更多的新对象。

缺陷

  • 对于包含引用类型值的属性来说,所有实例在默认的情况下都会取得相同的属性值。

实例

const user = {
    name:'小明',
    age:'30',
    getInfo(){
        console.log(`姓名:${this.name},年龄:${this.age}`)
    }
}
const xiaozhang = Object.create(user)
xiaozhang.name = '小张'
xiaozhang.age = 18

xiaozhang.getInfo() // 姓名:小张,年龄:18
user.getInfo() // 姓名:小明,年龄:30

4.适配器模式

适配器模式的目的是为了解决对象之间的接口不兼容的问题,通过适配器模式可以不更改源代码的情况下,让两个原本不兼容的对象在调用时正常工作。

优点

  1. 让任何两个没有关联的类可以同时有效运行,并且提高了复用性、透明度、以及灵活性

缺点

  • 过多的使用适配器模式,会让系统变得零乱,不易整体把控。建议在无法重构的情况下使用适配器。

实例

  • 拿一个现实中的例子来说,杰克只会英语,小明只会中文,它们在交流上出现了障碍,小红同时会中英双语,通过小红将杰克的英语翻译成中文,让小明和杰克进行无障碍的沟通,这里小红就起到了适配器的角色。
class Jack {
  english() {
    return 'I speak English'
  }
}
class Xiaoming {
  chinese() {
    return '我只会中文'
  }
}
// 适配器
class XiaoHong {
  constructor(person) {
    this.person = person
  }
  chinese() {
    return `${this.person.english()} 翻译: "我会说英语"`
  }
  english() {
    return  `${this.person.chinese()} 翻译: "我会说中文"`
  }
}
class Communication {
  speak(language) {
    console.log(language.chinese())
  }
}

const xiaoming = new Xiaoming()
const xiaoHong = new XiaoHong(new Jack())
const communication = new Communication()
communication.speak(xiaoming)
communication.speak(xiaoHong)

5.装饰器模式

装饰者模式能够在不更改源代码自身的情况下,对其进行职责添加。相比于继承装饰器的做法更轻巧。通俗的讲我们给心爱的手机上贴膜,带手机壳,贴纸,这些就是对手机的装饰。

优点

  1. 装饰类和被装饰类它们之间可以相互独立发展,不会相互耦合,装饰器模式是继承的一个替代模式,它可以动态的扩展一个实现类的功能。

缺点

  • 多层的装饰会增加复杂度

实例

  • 在编写飞机大战的游戏中,飞机对象的攻击方式只有普通子弹攻击,如何在不更改原代码的情况下,为它其他的攻击方式,如激光武器,导弹武器
class Aircraft {
    ordinary(){
        console.log('发射普通子弹')
    }
}
class AircraftDecorator {
    constructor(aircraft){
        this.aircraft = aircraft
    }
    laser(){
        console.log('发射激光')
    }
    guidedMissile(){
        console.log('发射导弹')
    }
    ordinary(){
        this.aircraft.ordinary()
    }
}
const aircraft = new Aircraft()
const aircraftDecorator = new AircraftDecorator(aircraft)
aircraftDecorator.ordinary() // 发射普通子弹
aircraftDecorator.laser() // 发射激光
aircraftDecorator.guidedMissile() // 发射导弹
// 可以看到在不更改源代码的情况下对它进行了装饰扩展

6.代理模式

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。

代理和本体接口需要一致性,代理和本体之间可以说是鸭子类型的关系,不在乎他怎么实现的,只要它们之间暴露的方法一致既可。

优点

  1. 职责清晰,高扩展性,智能化

缺点

  • 当对象和对象之间增加了代理可能会影响到处理的速度。
  • 实现代理需要额外的工作,有些代理会非常的复杂。

实例

我们都知道,领导拥有公司的最高权限,假设公司有员工100个,如果每个人都去找领导去处理事务,那领导肯定会崩溃,因此领导招聘了一个秘书帮他收集整理事务,秘书会在合适时间一次性将需要处理的业务交给老板处理,在这里秘书就是领导的一个代理角色。

// 员工
class Staff {
  constructor(affairType){
    this.affairType = affairType
  }
  applyFor(target){
    target.receiveApplyFor(this.affairType)
  }
}
// 秘书
class Secretary {
  constructor(){
    this.leader = new Leader()
  }
  receiveApplyFor(affair){
    this.leader.receiveApplyFor(affair)
  }
}
//领导
class Leader {
  receiveApplyFor(affair){
    console.log(`批准:${affair}`)
  }
}

const staff = new Staff('升职加薪')
staff.applyFor(new Secretary()) // 批准:升职加薪

7.外观模式

外观模式本质就是封装交互,隐藏系统的复杂性,提供一个可以访问的接口。由一个将子系统一组的接口集成在一起的高层接口,以提供一个一致的外观,减少外界与多个子系统之间的直接交互,从而更方便的使用子系统。

优点

  1. 减少系统的相互依赖,以及安全性和灵活性

缺点

  • 违反开放封闭原则,有变动的时候更改会非常麻烦,即使继承重构都不可行

实例

  • 外观模式经常被用于处理高级游览器的和低版本游览器的一些接口的兼容处理
function addEvent(el,type,fn){
    if(el.addEventlistener){// 高级游览器添加事件DOM API
        el.addEventlistener(type,fn,false)
    }else if(el.attachEvent){// 低版本游览器的添加事件API
        el.attachEvent(`on${type}`,fn)
    }else {//其他
        el[type] = fn
    }
}
  • 另一种场景,在某个函数中的某个参数可传可不传的情况下,通过函数重载的方式,让传参更灵活。
function bindEvent(el,type,selector,fn){
    if(!fn){
        fn = selector
    }
    // 其他代码
    console.log(el,type,fn)
}
bindEvent(document.body,'click','#root',()=>{})
bindEvent(document.body,'click',()=>{})

8.组合模式

组合模式就是由一些小的子对象构建出的更大的对象,而这些小的子对象本身可能也是由多个孙对象组合而成的。组合模式将对象组合成树状结构,以表示“部分-整体”的层次结构。除了用来表示树状结构之外,组合模式的另一个好处就是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。

优点

  1. 高层模块调用简单,节点可以自由添加

缺陷

  • 其叶对象和子对象声明都是实现类,而不是接口,这违反了依赖倒置原则

实例

以我们最常见的文件夹和文件的关系,非常适合用组合模式来描述,文件夹可以包括子文件夹和文件,文件不能包括任何文件,这种关系让最终会形成一棵树。下面来实现文件的添加,扫描该文件里的文件,并且可以删除文件。

// 文件夹类
class Folder {
  constructor(name) {
    this.name = name
    this.parent = null
    this.files = []
  }
  // 添加文件
  add(file) {
    file.parent = this
    this.files.push(file)
    return this
  }
  // 扫描文件
  scan() {
    console.log(`开始扫描文件夹:${this.name}`)
    this.files.forEach(file => {
      file.scan()
    });
  }
  // 删除指定文件
  remove() {
    if (!this.parent) {
      return
    }
    for (let files = this.parent.files, i = files.length - 1; i >= 0; i--) {
      const file = files[i]
      if (file === this) {
        files.splice(i, 1)
        break
      }
    }
  }
}
// 文件类
class File {
  constructor(name) {
    this.name = name
    this.parent = null
  }
  add() {
    throw new Error('文件下面不能添加任何文件')
  }
  scan() {
    console.log(`开始扫描文件:${this.name}`)
  }
  remove() {
    if (!this.parent) {
      return
    }
    for (let files = this.parent.files, i = files.length - 1; i >= 0; i++) {
      const file = files[i]
      if (file === this) {
        files.splice(i, 1)
      }
    }
  }
}

const book = new Folder('电子书')
const js = new Folder('js')
const node = new Folder('node')
const vue = new Folder('vue')
const js_file1 = new File('javascript高级程序设计')
const js_file2 = new File('javascript忍者秘籍')
const node_file1 = new File('nodejs深入浅出')
const vue_file1 = new File('vue深入浅出')

const designMode = new File('javascript设计模式实战')

js.add(js_file1).add(js_file2)
node.add(node_file1)
vue.add(vue_file1)

book.add(js).add(node).add(vue).add(designMode)
book.remove()
book.scan()

9.策略模式

策略模式指的是定义一系列算法,把他们一个个封装起来,目的就是将算法的使用和算法的实现分离开来。同时它还可以用来封装一系列的规则,比如常见的表单验证规则,只要这些规则指向的目标一致,并且可以被替换使用,那么就可以用策略模式来封装它们。

优点

  1. 算法可以自由切换,避免了使用多层条件判断,增加了扩展性

缺陷

  • 策略类增多,所有策略类都需要对外暴露。

实例

刚入这个行业的时候,写表单验证经常无止境的if...else写法,意识到这种写法不靠谱,于是我把检验规则放在一个对象中,在函数中对它进行控制,把规则与实现进行了分离,每次只需要在封装的规则中去修改配置。在后面的多种场景都用这种方法,解决了频繁使用if...else的问题,当第一次接触倒策略模式才知道这种写法也算策略模式。

const rules = {
    cover_img: {
        must: false,
        msg: '请上传封面图片',
        val: ''
    },
    name: {
        must: true,
        msg: '姓名不能为空',
        val: ''
    },
    sex: {
        must: true,
        msg: '请填写性别',
        val: ''
    },
    birthday: {
        must: false,
        msg: '请选择生日',
        val: ''
    },
}
function verify(){
    for(const key in rules){
        if(rules[key].must&&!rules[key].val){
            console.log(rules[key].msg)
        }
    }
}
verify()
// 姓名不能为空
// 请填写性别

10.备忘录模式

备忘录模式就是在不破坏封装的前提下,捕获一个对象内部状态,并在该对象之外保存这个状态,以保证以后可以将对象恢复到原先的状态。

优点

  1. 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
  2. 实现了信息的封装,使得用户不需要关心状态的保存细节

缺陷

  • 如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。

实例

// 棋子
class ChessPieces {
  constructor(){
    this.chess = {}
  }
  // 获取棋子
  getChess(){
    return this.chess
  }
}
// 记录棋路
class Record {
  constructor(){
    this.chessTallyBook = [] // 记录棋路
  }
  recordTallyBook(chess){
    const isLoadtion = this.chessTallyBook.some(
      item=>item.location === chess.location
    )
    if(isLoadtion){
      console.log(`${chess.type},${chess.location}已存在其他棋子`)
    }else {
      this.chessTallyBook.push(chess)
    }
  }
  getTallyBook(){
    return this.chessTallyBook.pop()
  }
}

// 下棋规则
class ChessRule {
  constructor(){
    this.chessInfo = {}
  }
  playChess(chess){
    this.chessInfo = chess
  }
  getChess(){
    return this.chessInfo
  }
  // 记录棋路
  recordTallyBook(){
    return new ChessPieces(this.chessInfo)
  }
  // 悔棋
  repentanceChess(chess){
    this.chessInfo = chess.getTallyBook()
  }
}

const chessRule = new ChessRule()
const record = new Record()

chessRule.playChess({
  type:'黑棋',
  location:'X10,Y10'
})
record.recordTallyBook(chessRule.getChess())//记录棋路
chessRule.playChess({
  type:'白棋',
  location:'X11,Y10'
})
record.recordTallyBook(chessRule.getChess())//记录棋路
chessRule.playChess({
  type:'黑棋',
  location:'X11,Y11'
})
record.recordTallyBook(chessRule.getChess())//记录棋路
chessRule.playChess({
  type:'白棋',
  location:'X12,Y10'
})
console.log(chessRule.getChess())//{type:'白棋',location:'X12,Y10'}
chessRule.repentanceChess(record) // 悔棋
console.log(chessRule.getChess())//{type:'黑棋',location:'X11,Y11'}
chessRule.repentanceChess(record) // 悔棋
console.log(chessRule.getChess())//{type:'白棋',location:'X11,Y10'}

11.观察者模式

观察者模式定义一种一对多的关系,让多个观察者对象同时监听某一个目标对象。这里的我们称为目标对象(Subject),它有增加/删除/通知等方法,当这个主题对象的状态发生变化时就会通知所有的观察者对象;而则称为观察者对象(Observer),它可以接收目标对象(Subject)的状态改变并进行处理状态改变产生的行为;当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。

优点

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

缺陷

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

实例

//这里定义了目标对象和观察者对象两个类,在目标对象中维护了一个观察者的数组,新增观察者时将观察者向数组中push;当目标状态改变(调用方法setState)时,会通过notifyAllObservers通知所有的观察者,并调用观察者中的update函数;
// 目标对象, 保存状态,状态变化之后通知所有观察者对象
class Subject {
  constructor() {
    this.state = 0
    this.observers = []
  }
  getState() {
    return this.state
  }
  setState(state) {
    this.state = state
    this.notifyAllObservers()
  }
  //通知所有观察者
  notifyAllObservers() {
    this.observers.forEach(observer => {
      observer.update()
    })
  }
  attach(observer) {
    //添加观察者
    this.observers.push(observer)
  }
}

// 观察者
class Observer {
  constructor(name, subject) {
    this.name = name
    this.subject = subject
    this.subject.attach(this)
  }
  update() {
    console.log(`${this.name} update, state: ${this.subject.getState()}`)
  }
}


let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('02', s)

s.setState(12)


//这种模式的应用在日常中也很常见,比如我们给div绑定click监听事件(DOM事件),其本质就是观察者模式的一种应用
//btn可以看作是我们的目标对象(被观察对象),当它被点击时,也就是它的状态发生了变化,那么它就会通知内部添加的观察者对象,也就是我们通过addEventListener函数添加的两个匿名函数。
我们发现,观察者模式好处是能够降低耦合,目标对象和观察者对象逻辑互不干扰,两者都专注于自身的功能,只提供和调用了更新接口;而缺点也很明显,在目标对象中维护的所有观察者都能接收到通知,无法进行过滤筛选。

var btn = document.getElementById('btn')
btn.addEventListener('click', function(ev){
  console.log(1)
})
btn.addEventListener('click', function(ev){
  console.log(2)
})

12.发布订阅模式

发布订阅模式,同样定义对象之间的一对多的关系,刚开始发布订阅模式只是观察者模式的一个别称,但是经过时间的沉淀,他改进了观察者模式的缺点,渐渐地开始独立于观察者模式。发布对象(Publisher)我们可以类比为观察者模式中的目标对象,来发布事件通知;接收对象(Subscriber)可以类比为观察者对象,订阅各种通知。发布订阅模式和观察者模式的不同在于,增加了第三方即事件通道。观察者模式把观察者对象维护在目标对象中的,需要发布消息时直接发消息给观察者。在观察者模式中,目标对象本身是知道观察者存在的。而发布订阅模式中,发布者并不维护订阅者,也不知道订阅者的存在,所以也不会直接通知订阅者,而是通知事件通道,由事件通道通知订阅者。
【八】设计模式_第1张图片

优点

  1. 观察者和被观察者它们之间是抽象耦合的,并且建立了触发机制

缺陷

  • 当订阅者比较多的时候,同时通知所有的订阅者可能会造成性能问题。
  • 在订阅者和订阅目标之间如果循环引用执行,会导致崩溃。
  • 发布订阅模式没有办法提供给订阅者所订阅的目标它是怎么变化的,仅仅只知道它变化了。

实例

为了加深理解,我们以生活中的情形为例;比如我们订阅报纸杂志等,一般不会直接跑到报社去订阅,而是通过一个平台,比如街边的报亭或者邮局也可以订阅;我们订阅报纸后报社出版后会通过平台来给我们投递,通过邮局邮寄或者自取等等,那么这里就涉及到了报社、订阅者和第三方平台三个对象,我们通过代码来模拟三者的动作:

// 报社
class Publisher {
  constructor(name, channel) {
    this.name = name;
    this.channel = channel;
  }
  // 注册报纸
  addTopic(topicName) {
    this.channel.addTopic(topicName);
  }
  // 推送报纸
  publish(topicName) {
    this.channel.publish(topicName);
  }
}
// 订阅者
class Subscriber {
  constructor(name, channel) {
    this.name = name;
    this.channel = channel;
  }
  //订阅报纸
  subscribe(topicName) {
    this.channel.subscribeTopic(topicName, this);
  }
  //取消订阅
  unSubscribe(topicName) {
    this.channel.unSubscribeTopic(topicName, this);
  }
  //接收推送
  update(topic) {
    console.log(`${topic}已经送到${this.name}家了`);
  }
}
// 第三方平台
class Channel {
  constructor() {
    this.topics = {};
  }
  //报社在平台注册报纸
  addTopic(topicName) {
    this.topics[topicName] = [];
  }
  //报社取消注册
  removeTopic(topicName) {
    delete this.topics[topicName];
  }
  //订阅者订阅报纸
  subscribeTopic(topicName, sub) {
    if (this.topics[topicName]) {
      this.topics[topicName].push(sub);
    }
  }
  //订阅者取消订阅
  unSubscribeTopic(topicName, sub) {
    this.topics[topicName].forEach((item, index) => {
      if (item === sub) {
        this.topics[topicName].splice(index, 1);
      }
    });
  }
  //平台通知某个报纸下所有订阅者
  publish(topicName) {
    this.topics[topicName].forEach((item) => {
      item.update(topicName);
    });
  }
}

这里的报社我们可以理解为发布者(Publisher)的角色,订报纸的读者理解为订阅者(Subscriber),第三方平台就是事件通道;报社在平台上注册某一类型的报纸,然后读者就可以在平台订阅这种报纸;三个类准备好了,我们来看下他们彼此如何进行联系:

var channel = new Channel();

var pub1 = new Publisher("报社1", channel);
var pub2 = new Publisher("报社2", channel);

pub1.addTopic("晨报1");
pub1.addTopic("晚报1");
pub2.addTopic("晨报2");

var sub1 = new Subscriber("小明", channel);
var sub2 = new Subscriber("小红", channel);
var sub3 = new Subscriber("小张", channel);

sub1.subscribe("晨报1");
sub2.subscribe("晨报1");
sub2.subscribe("晨报2");
sub3.subscribe("晚报1");

sub3.subscribe("晨报2");
sub3.unSubscribe("晨报2");

pub1.publish("晨报1");
pub1.publish("晚报1");
pub2.publish("晨报2");

//晨报1已经送到小明家了
//晨报1已经送到小红家了
//晚报1已经送到小张家了
//晨报2已经送到小红家了

最后

走过路过,不要错过,点赞、收藏、评论三连~

你可能感兴趣的:(前端)