《消失的设计模式》系列之观察者模式

2020-02-16 21:35:42 +08:00
 EclipsePrayer

设计模式是面向对象的有用工具,但是编程语言的发展和多种编程范式混合编程的可能,使很多的模式被语言特性取代,或者被其他编程范式解决。

要解决的问题

假如你想创建一个机器人,在发布文章的时候,自动同步到知乎、简书等其他的平台。在这里,我们有 3 个实体,一个是发布文章,在该事件发生的时候,分别通知知乎和简书的实体进行同步。同时为了可扩展性,需要支持可能的其他的平台。为了解决这类问题,我们可以使用观察者模式。

定义

在对象之间定义一对多的依赖,当一个对象改变时,依赖它的对象都会收到通知,并自动更新。

面向对象的方式

UML

如上图所示,

代码

首先定义 Observer 接口:

interface Observer {
  update(newBlog: string)
}

包含一个 update 方法。

接下来分别定义两个 Observer

class ZhihuObserver implements Observer {
  update(newBlog) {
    console.log('publishing to zhihu...', newBlog)
  }
}

class JianshuObserver implements Observer {
  update(newBlog) {
    console.log('publishing to jianshu...', newBlog)
  }
}

在收到通知(update 方法被调用)的时候,将新文章的内容打印出来。

定义 Subject 接口:

interface Subject {
  registerObserver(o: Observer)
  removeObserver(o: Observer)
  notifyObservers(newBlog: string)
}

这里由于我们需要告知观察者新文章的内容,notifyObservers 接受了一个 string 类型的参数。

class BlogWriter implements Subject {
  private observers: Observer[] = []

  registerObserver(o: Observer) {
    this.observers.push(o)
  }
  removeObserver(o: Observer) {
    this.observers = this.observers.filter(v => v !== o)
  }
  notifyObservers(blog: string) {
    this.observers.forEach(o => {
      o.update(blog)
    })
  }
}

BlogWriter 类实现了 Subject 接口:

来看运行效果:

const subject: Subject = new BlogWriter()
const zhihu = new ZhihuObserver()
subject.registerObserver(zhihu)
subject.registerObserver(new JianshuObserver())
subject.notifyObservers('hello')
// publishing to zhihu... hello
// publishing to jianshu... hello
subject.removeObserver(zhihu)
subject.notifyObservers('world')
// publishing to jianshu... world

面向对象完整代码

戴上函数式的思考帽

其实问题的本质是将一系列的执行过程存储起来,在特定事件发生的时候,执行这些过程。

我们来修改 Observer 的定义:

type Observer = (newBlog: string) => void

非常直白,就是一个函数定义。那么对应的 Observer 就可以改成:

const zhihuObserver: Observer = newBlog => {
  console.log('publishing to zhihu...', newBlog)
}

const jianshuObserver: Observer = newBlog => {
  console.log('publishing to jianshu...', newBlog)
}

就是两个函数定义而已。此时 BlogWriter 类变成了:

class BlogWriter implements Subject {
  private observers: Observer[] = []
  registerObserver(o: Observer) {
    this.observers.push(o)
  }
  removeObserver(o: Observer) {
    this.observers = this.observers.filter(v => v !== o)
  }
  notifyObservers(blog: string) {
    this.observers.forEach(o => {
      o(blog)
    })
  }
}

实际上,我们可以更进一步,去掉类的枷锁:

const createBlogWriter = (): Subject => {
  let observers: Observer[] = []
  return {
    registerObserver: (o: Observer) => {
      observers.push(o)
    },
    removeObserver: (o: Observer) => {
      observers = observers.filter(v => v !== o)
    },
    notifyObservers(blog: string) {
      observers.forEach(o => o(blog))
    },
  }
}

这里我们创建了一个函数,createBlogWriter 该函数的返回值是一个实现了 Subject 接口的对象。代码逻辑和之前面向对象的方式相同,不过这里我们使用了闭包来承担私有变量的作用。来看最终的运行效果:

const subject = createBlogWriter()
subject.registerObserver(zhihuObserver)
subject.registerObserver(jianshuObserver)
subject.notifyObservers('hello')
// publishing to zhihu... hello
// publishing to jianshu... hello
subject.removeObserver(zhihuObserver)
subject.notifyObservers('world')
// publishing to jianshu... world

我们再来看一遍函数式的代码:

const createBlogWriter = (): Subject => {
  let observers: Observer[] = []
  return {
    registerObserver: (o: Observer) => {
      observers.push(o)
    },
    removeObserver: (o: Observer) => {
      observers = observers.filter(v => v !== o)
    },
    notifyObservers(blog: string) {
      observers.forEach(o => o(blog))
    },
  }
}

非常简洁。而这里真正的强大之处在于,Observer 只是一个函数类型,任何接收一个类型为 string -> void 的函数都可以作为 Observer

函数式完整代码

总结

本着 do not call me, I will call you! 的理念,观察者模式可以在其他对象发生某些变化的时候得到通知。其本质是存储计算过程,稍后执行,换句话说,就是将一些函数存下来,在适当的时候调用而已,如此简单。而越来越多的语言将函数视为一等对象,所以函数作为参数传入,存储,再执行这种模式会非常简单易用。

3435 次点击
所在节点    编程
6 条回复
lhx2008
2020-02-16 21:54:16 +08:00
代码看不太懂,不过所谓观察者模式,责任链模式,策略模式,装饰器模式都是差不多的,本质上是在执行端的可插拔的函数,只是有些人把面向对象的那些叫做设计模式,面向过程的叫函数式编程
bugDev
2020-02-16 22:06:19 +08:00
写的很好,看着很舒服
darksword21
2020-02-17 00:18:25 +08:00
我还以为会在最下面看到微信公众号
EclipsePrayer
2020-02-17 10:07:34 +08:00
@darksword21 哈哈,欢迎关注公众号呀~ lambdaIO
EclipsePrayer
2020-02-17 10:07:49 +08:00
@bugDev 谢谢!
EclipsePrayer
2020-02-17 10:10:44 +08:00
@lhx2008 “本质上是在执行端的可插拔的函数” 对的!
代码是 TypeScript,如果有没写明白的地方,还请指出

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/645088

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX