V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
EclipsePrayer
V2EX  ›  编程

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

  •  1
     
  •   EclipsePrayer · 2020-02-16 21:35:42 +08:00 · 3496 次点击
    这是一个创建于 1748 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    要解决的问题

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

    定义

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

    面向对象的方式

    UML

    uml

    如上图所示,

    • Subject 接口定义了主题的行为,在我们的例子中,是发布文章。它持有一系列遵循 Observer 接口的实例。可以通过 registerObserver 进行添加,removeObser 进行移除,最终通过 notifyObservers 通知所有的观察者
    • Observer 接口定义了观察者的行为

    代码

    首先定义 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 接口:

    • 拥有一个私有的 observers 来存储已注册的 Observer。
    • registerObserver 方法将一个 Observer 存储到 observers 数组中
    • removeObserver 方法将指定的 Observer 移除
    • notifyObservers 方法通知所有的 observers(调用其 update 方法)

    来看运行效果:

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

    6 条回复    2020-02-17 10:10:44 +08:00
    lhx2008
        1
    lhx2008  
       2020-02-16 21:54:16 +08:00 via Android
    代码看不太懂,不过所谓观察者模式,责任链模式,策略模式,装饰器模式都是差不多的,本质上是在执行端的可插拔的函数,只是有些人把面向对象的那些叫做设计模式,面向过程的叫函数式编程
    bugDev
        2
    bugDev  
       2020-02-16 22:06:19 +08:00 via iPhone
    写的很好,看着很舒服
    darksword21
        3
    darksword21  
       2020-02-17 00:18:25 +08:00 via iPhone   ❤️ 1
    我还以为会在最下面看到微信公众号
    EclipsePrayer
        4
    EclipsePrayer  
    OP
       2020-02-17 10:07:34 +08:00
    @darksword21 哈哈,欢迎关注公众号呀~ lambdaIO
    EclipsePrayer
        5
    EclipsePrayer  
    OP
       2020-02-17 10:07:49 +08:00
    @bugDev 谢谢!
    EclipsePrayer
        6
    EclipsePrayer  
    OP
       2020-02-17 10:10:44 +08:00
    @lhx2008 “本质上是在执行端的可插拔的函数” 对的!
    代码是 TypeScript,如果有没写明白的地方,还请指出
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2693 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 12:24 · PVG 20:24 · LAX 04:24 · JFK 07:24
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.