观察者模式有一个“别名”,叫发布 - 订阅模式(之所以别名加了引号,是因为两者之间存在着细微的差异,下个小节里我们会讲到这点)。这个别名非常形象地诠释了观察者模式里两个核心的角色要素——“发布者”与“订阅者”
在上述的过程中,需求文档(目标对象)的发布者只有一个——产品经理韩梅梅。而需求信息的接受者却有多个——前端、后端、测试同学,这些同学的共性就是他们需要根据需求信息开展自己后续的工作、因此都非常关心这个需求信息,于是不得不时刻关注着这个群的群消息提醒,他们是实打实的订阅者,即观察者对象。
现在我们再回过头来看一遍开头我们提到的略显抽象的定义:
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
在我们上文这个钉钉群里,一个需求信息对象对应了多个观察者(技术同学),当需求信息对象的状态发生变化(从无到有)时,产品经理通知了群里的所有同学,以便这些同学接收信息进而开展工作:角色划分 —> 状态变化 —> 发布者通知到订阅者,这就是观察者模式的“套路”。

在实践中理解定义

结合我们上面的分析,现在大家知道,在观察者模式里,至少应该有两个关键角色是一定要出现的——发布者和订阅者。用面向对象的方式表达的话,那就是要有两个类
首先我们来看这个代表发布者的类,我们给它起名叫Publisher。这个类应该具备哪些“基本技能”呢?大家回忆一下上文中的韩梅梅,韩梅梅的基本操作是什么?首先是拉群(增加订阅者),然后是@所有人(通知订阅者),这俩是最明显的了。此外作为群主&产品经理,韩梅梅还具有踢走项目组成员(移除订阅者)的能力。OK,产品经理发布者类的三个基本能力齐了,下面我们开始写代码:

  1. // 定义发布者类
  2. class Publisher {
  3. constructor() {
  4. this.observers = []
  5. console.log('Publisher created')
  6. }
  7. // 增加订阅者
  8. add(observer) {
  9. console.log('Publisher.add invoked')
  10. this.observers.push(observer)
  11. }
  12. // 移除订阅者
  13. remove(observer) {
  14. console.log('Publisher.remove invoked')
  15. this.observers.forEach((item, i) => {
  16. if (item === observer) {
  17. this.observers.splice(i, 1)
  18. }
  19. })
  20. }
  21. // 通知所有订阅者
  22. notify() {
  23. console.log('Publisher.notify invoked')
  24. this.observers.forEach((observer) => {
  25. observer.update(this)
  26. })
  27. }
  28. }

ok,搞定了发布者,我们一起来想想订阅者能干啥——其实订阅者的能力非常简单,作为被动的一方,它的行为只有两个——被通知、去执行(本质上是接受发布者的调用,这步我们在Publisher中已经做掉了)。既然我们在Publisher中做的是方法调用,那么我们在订阅者类里要做的就是方法的定义

  1. // 定义订阅者类
  2. class Observer {
  3. constructor() {
  4. console.log('Observer created')
  5. }
  6. update() {
  7. console.log('Observer.update invoked')
  8. }
  9. }

以上,我们就完成了最基本的发布者和订阅者类的设计和编写。在实际的业务开发中,我们所有的定制化的发布者/订阅者逻辑都可以基于这两个基本类来改写。比如我们可以通过拓展发布者类,来使所有的订阅者来监听某个特定状态的变化。仍然以开篇的例子为例,我们让开发者们来监听需求文档(prd)的变化:

  1. // 定义一个具体的需求文档(prd)发布类
  2. class PrdPublisher extends Publisher {
  3. constructor() {
  4. super()
  5. // 初始化需求文档
  6. this.prdState = null
  7. // 韩梅梅还没有拉群,开发群目前为空
  8. this.observers = []
  9. console.log('PrdPublisher created')
  10. }
  11. // 该方法用于获取当前的prdState
  12. getState() {
  13. console.log('PrdPublisher.getState invoked')
  14. return this.prdState
  15. }
  16. // 该方法用于改变prdState的值
  17. setState(state) {
  18. console.log('PrdPublisher.setState invoked')
  19. // prd的值发生改变
  20. this.prdState = state
  21. // 需求文档变更,立刻通知所有开发者
  22. this.notify()
  23. }
  24. }

作为订阅方,开发者的任务也变得具体起来:接收需求文档、并开始干活:

  1. class DeveloperObserver extends Observer {
  2. constructor() {
  3. super()
  4. // 需求文档一开始还不存在,prd初始为空对象
  5. this.prdState = {}
  6. console.log('DeveloperObserver created')
  7. }
  8. // 重写一个具体的update方法
  9. update(publisher) {
  10. console.log('DeveloperObserver.update invoked')
  11. // 更新需求文档
  12. this.prdState = publisher.getState()
  13. // 调用工作函数
  14. this.work()
  15. }
  16. // work方法,一个专门搬砖的方法
  17. work() {
  18. // 获取需求文档
  19. const prd = this.prdState
  20. // 开始基于需求文档提供的信息搬砖。。。
  21. ...
  22. console.log('996 begins...')
  23. }
  24. }

下面,我们可以 new 一个 PrdPublisher 对象(产品经理),她可以通过调用 setState 方法来更新需求文档。需求文档每次更新,都会紧接着调用 notify 方法来通知所有开发者,这就实现了定义里所谓的:
目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新

OK,下面我们来看看韩梅梅和她的小伙伴们是如何搞事情的吧:

  1. // 创建订阅者:前端开发李雷
  2. const liLei = new DeveloperObserver()
  3. // 创建订阅者:服务端开发小A(sorry。。。起名字真的太难了)
  4. const A = new DeveloperObserver()
  5. // 创建订阅者:测试同学小B
  6. const B = new DeveloperObserver()
  7. // 韩梅梅出现了
  8. const hanMeiMei = new PrdPublisher()
  9. // 需求文档出现了
  10. const prd = {
  11. // 具体的需求内容
  12. ...
  13. }
  14. // 韩梅梅开始拉群
  15. hanMeiMei.add(liLei)
  16. hanMeiMei.add(A)
  17. hanMeiMei.add(B)
  18. // 韩梅梅发送了需求文档,并@了所有人
  19. hanMeiMei.setState(prd)

以上,就是观察者模式在代码世界里的完整实现流程了。