TS 设计模式之观察者模式

·  阅读 386

这一篇我们延续之前的风格,用故事的形式讲解设计模式。

share.jpg

小凯蒂一家生活在美国海岸线旁边一个小镇上,在她的小镇,没有楼房,每家每户都是漂亮的小房子。她的爸妈是给小镇里送牛奶的,一到凌晨五点钟,她爸妈就起床开始给每家每户送牛奶,送好牛奶之后,她们就回到家里,使用取牛奶通知软件,告诉大家牛奶送到了,要趁还没变质之前赶紧取走。

小手一按,订牛奶的家庭就会收到取牛奶的通知。

image.png

订牛奶的家庭会在客厅安装一个显示屏,每次凯蒂家通知他们的时候,显示屏就会告诉他们要去取牛奶。

当时给小凯蒂家安装取牛奶机的大叔很忙,就随手给他们设计了一下,他们的牛奶机目前是这样子的:

interface Milk {
  kind: string;
  ml: number;
}

class MinkCenter {
  milkList: Milk[]

  constructor() {
    this.milkList = []
  }

  receiveMilk(milkList: Milk[]) {
    console.log('收到供奶站送来的牛奶,装车')
    this.milkList = milkList
  }
  sendMilkToPeople() {
    console.log("把牛奶送到各家各户")
    this.notifyPeopleToTakeMilk()
    this.milkList = []
  }

  notifyPeopleToTakeMilk() {
    console.log("通知各家各户取牛奶")
    const dainel = new Family('dainel')
    const jeinny = new Family('jeinny')
    const wayne = new Family('wayne')
    dainel.update(); // 通知各位取牛奶
    jeinny.update()
    wayne.update()
  } 
}
复制代码

notifyPeopleToTakeMilk 内部,我们会通知订牛奶的家庭去取牛奶,每个家庭的类也很简单:

class Family {
  name: string;

  constructor(name: string) {
    this.name = name
  }
  update() {
    console.log(this.name + '您好,牛奶送到了,快去取吧')
  }
}
复制代码

最后,我们的运行这个取牛奶机的代码长这样:

const milkCenter = new MilkCenter();
const milkList = [
  { kind: "纯牛奶", ml: 200 },
  { kind: "纯牛奶", ml: 300 },
  { kind: "纯牛奶", ml: 500 },
];

milkCenter.receiveMilk(milkList);
milkCenter.sendMilkToPeople();
milkCenter.notifyPeopleToTakeMilk();
复制代码

运行这段代码,我们的结果是这样子的:

image.png

MilkCenter 就是小凯蒂家的取牛奶通知机。一个个 Family 对象就是放在订牛奶的家里显示屏。

目前来说,订牛奶的家庭都使用代码写在了 MilkCenter 内部的 notifyPeopleToTakeMilk 函数里面。

由于订牛奶的家庭是硬编码(hardcode)进 MilkCenter 的,就没法在运行的时候改。每次有新的家庭要来订牛奶了,小凯蒂的爸妈要把取牛奶机停下来,把新订牛奶的家庭加进来;有的家庭因为出去旅游要暂停订牛奶了,也要先把取牛奶机停下来,把不订牛奶的家庭暂时移除掉。

看下面这个示例,有个叫 padim 的伙计的要订牛奶啦:

notifyPeopleToTakeMilk() {
    console.log("通知各家各户取牛奶");
    // ...
    const padim = new Family('padim'); // 这个是新加的
    // ...
    padim.update();
  } 
复制代码

刚开始人少的时候用起来还不错,隔几天才有几个人来更改一下,随着他们一家的牛奶生意越做越大,几乎每天都人来改,他们家从早忙到晚,拆牛奶机,安牛奶机,连觉都睡不好。

小凯蒂看着爸妈这么累,有一天她就坐在窗前许愿:神奇的魔法师,我给你一个我最爱的棒棒糖,有办法能让我爸妈的工作轻松一点吗?

当她许完愿,睁开眼睛,一只带着白帽子的猫头鹰先生慢悠悠的飞了过来。

猫头鹰先生双脚站在窗户边上:"听到你的许愿了,小事一桩嘛!"

说着,从口袋里掏出一个图纸:"可爱的小凯蒂,你就照着这个做就好啦!好用的话,记得给我棒棒糖呀!" 说完猫头鹰先生就摇摇翅膀飞走了。

小凯蒂拿起那张图纸:"咦,这是什么,上面写着「观察者模式」,好奇怪的名字,有用吗?"

小凯蒂半信半疑的按照图纸做了起来。

上面写着,首先要定义两个接口,一个叫做 Subject,一个叫做 Observer,这个 Subject 是给小凯蒂家用的,Observer 是给订牛奶的家庭用的,快来看看他们的样子:

interface Observer {
  update: () => void; 
}

interface Subject {
  registerObserver: (o: Observer) => void;
  removeObserver: (o: Observer) => void;
  notifyObservers: () => void;
}
复制代码

小凯蒂建好这两个接口后,发现还不算难,然后就去改造了家庭类,发现,也几乎不需要修改什么,只要把原先的类实现一下 Observer 这个接口就好了,她特别开心:

class Family implements Observer {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
  update() {
    console.log(this.name + "您好,牛奶送到了,快去取吧");
  }
}
复制代码

接下来,她来改造会有一丢丢麻烦的 MinkCenter 类。她增加了一个变量 observers 维护所有订牛奶的家庭,还实现了 Subject 接口的三个方法。

class MilkCenter implements Subject {
  milkList: Milk[]
  observers: Observer[]

  constructor() {
    this.milkList = []
    this.observers = []
  }

  registerObserver(o: Observer) {
    this.observers.push(o)
  }
  removeObserver(o: Observer){
    const targetIndex = this.observers.findIndex(family => family === o)
    this.observers.splice(targetIndex, 1)
  }
  notifyObservers(){
    this.observers.forEach(family => {
      family.update()
    })
  }

  receiveMilk(milkList: Milk[]) {
    console.log("收到供奶站送来的牛奶,装车");
    this.milkList = milkList
  }
  sendMilkToPeople() {
    console.log("把牛奶送到各家各户")
    this.milkList = []
  }

  notifyPeopleToTakeMilk() {
    console.log("通知各家各户取牛奶")
    this.notifyObservers()
  }
}
复制代码

小凯蒂写完这段代码,开始意识到,猫头鹰先生的图纸真是一个好东西。之前那个很长的notifyPeopleToTakeMilk 函数不见了,取而代之的是直接调用 notifyObservers 方法。

当有了新的家庭加入进来,我们就调用 registerObserver 去把家庭加到订购列表中去,由于我们 的家庭都实现了 Observer 接口,这就让我们添加、删除、遍历都能统一的操作。

经过这样的改进,小凯蒂的爸妈再也不用停机修改牛奶机了,他们在牛奶机上加了一个输入框和按钮,点击这个按钮,就会把输入框的值作为家庭的名字,新建一个家庭对象,然后再调用 registerObserver 方法把家庭对象加入到观察者列表。

每当有新的家庭来订牛奶,只需要在牛奶机运行的时候调用 registerObserver 就好了,下次通知取牛奶的时候就会通知到新的家庭!

const milkCenter = new MilkCenter()
const dainel = new Family("dainel")
const jeinny = new Family("jeinny")
const wayne = new Family("wayne")

milkCenter.registerObserver(dainel)
milkCenter.registerObserver(jeinny)
milkCenter.registerObserver(wayne)

const milkList = [
  { kind: "纯牛奶", ml: 200 },
  { kind: "纯牛奶", ml: 300 },
  { kind: "纯牛奶", ml: 500 },
];

milkCenter.receiveMilk(milkList)
milkCenter.sendMilkToPeople()
milkCenter.notifyPeopleToTakeMilk()
复制代码

不仅如此,如果有一天通知取牛奶的机器坏掉了,换一个就好了,但是之前的就不行,因为订阅用户信息都存在牛奶机里面(也就是 MilkCenter 内部)。但是经过我们的改造,我们的主题(MilkCenter)和诸多观察者(各个 Family 的实例)耦合度降到了最低,坏了,换!

我们知道,有的净水机上有童锁按钮,为的就是避免小孩子误触后接到了热水。由于小凯蒂家牛奶机很新颖,经常有小调皮来捣乱。于是小凯蒂还打算加一个「童锁」功能,不让每次点击通知按钮都通知各个家庭。

怎么做呀?巧了,猫头鹰先生给的图纸上也有!

原来要加一个 setChanged 方法就好了,来看看怎么用吧:

class MilkCenter implements Subject {
  hasChanged: boolean

  constructor() {
    // ... 省略不重要的 ...
    this.hasChanged = false
  }

  setChanged() {
    this.hasChanged = true
  }

  // ... 省略不重要的 ...
  notifyObservers(){
    if (!this.hasChanged) {
      return
    }
    this.observers.forEach(family => {
      family.update()
    })

    this.hasChanged = false
  }
}
复制代码

加了 setChanged 之后,我们要在送完奶之后,调用一下它才能让订奶的家庭收到通知,被误触的问题就轻松解决啦。观察者模式的一个缺点就是有时候通知太灵活,使用刚刚那种方案就能一定程度上减少这个问题的发生。

milkCenter.setChanged()
milkCenter.notifyPeopleToTakeMilk()
复制代码

小凯蒂很开心,她的愿望都实现了,第二天,猫头鹰先生如愿得到了他的棒棒糖,含在嘴里,悠哉悠哉的走了 ~


上面就是观察者模式,是不是觉得比较简单呢?

今天和你讨论的模式估计是在前端范围最广的设计模式了,我相信你也肯定使用过。毕竟,当你写下第一个事件监听函数的时候就代表你已经使用过了:

document.addEventListener('#app', () => {
    console.log('click me')
})
复制代码

我们来看一下官方一点的解释:

观察者模式软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常通过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

传统的观察者模式我们可能比较少使用,在前端,我们可能更多的是使用它的变种之一:发布订阅模式。

实现发布订阅的库也很多,比如 mitt

import mitt from 'mitt'

const emitter = mitt()

emitter.on('foo', e => console.log('foo', e) )
emitter.emit('foo', { a: 'b' })
复制代码

传统的观察者模式和发布-订阅机制也略微有一点不同。前者倾向于一对多,后者更倾向于一对一。在传统的观察者模式中,我们要在主题(Subject)中去显示的注册观察者(Observer ),并且我们的观察者要实现特定的接口。但是发布-订阅机制就松散的多了,emitter.on 的接受的回调比较随意,只要是一个回调函数就好,具体处理就自己酌情而定,这也导致了,它在 TypeScript 中不是那么好写类型。

好了,这就是本文的全部内容啦,我们重点讲解了如何实现一个观察者模式,接下来简单对比了一下观察者模式和发布-订阅模式。希望看得还算开心 :)

分类:
前端
收藏成功!
已添加到「」, 点击更改