一文读懂观察者模式

01 意图

观察者是一种行为设计模式,允许您定义订阅机制来通知多个对象他们正在观察的对象发生的任何事件。

02 问题

假设您有两种类型的对象:aCustomer和 a Store。客户对某个特定品牌的产品(例如,它是 iPhone 的新型号)非常感兴趣,该产品应该很快就会在商店中销售。

客户可以每天访问商店并检查产品可用性。但是,尽管产品仍在运输途中,但这些旅行中的大多数都是毫无意义的。

另一方面,每次有新产品上市时,商店都会向所有客户发送大量电子邮件(可能被视为垃圾邮件)。这将使一些顾客免于无休止地前往商店。同时,它也会让其他对新产品不感兴趣的客户感到不安。

看来我们发生了冲突。要么客户浪费时间检查产品可用性,要么商店浪费资源通知错误的客户。

03 解决方案

具有某些有趣状态的对象通常称为subject,但由于它也会通知其他对象有关其状态的更改,因此我们将其称为publisher。所有其他想要跟踪发布者状态更改的对象都称为订阅者

观察者模式建议您向发布者类添加订阅机制,以便各个对象可以订阅或取消订阅来自该发布者的事件流。不要怕!一切并不像听起来那么复杂。实际上,这种机制包括 1) 一个数组字段,用于存储对订阅者对象的引用列表和 2) 几个公共方法,这些方法允许将订阅者添加到该列表中并从该列表中删除它们。

现在,每当发布者发生重要事件时,它都会遍历其订阅者并在其对象上调用特定的通知方法。

真实的应用程序可能有几十个不同的订阅者类,它们对跟踪同一发布者类的事件感兴趣。您不希望将发布者耦合到所有这些类。此外,如果您的发布者类应该被其他人使用,您甚至可能事先不知道其中的一些。

这就是为什么所有订阅者都实现相同的接口并且发布者仅通过该接口与他们进行通信至关重要的原因。此接口应声明通知方法以及一组参数,发布者可以使用这些参数将一些上下文数据与通知一起传递。

如果您的应用有多种不同类型的发布者,并且您希望订阅者与所有发布者兼容,您可以更进一步,让所有发布者遵循相同的界面。这个接口只需要描述几个订阅方法。该接口将允许订阅者观察发布者的状态,而无需耦合到他们的具体类。

04 举个栗子

如果您订阅报纸或杂志,您不再需要去商店查看下一期是否有货。相反,出版商会在出版后将通知直接发送到您的邮箱。

出版商维护着一份订阅者列表,并且知道他们对哪些杂志感兴趣。订阅者可以在他们希望阻止出版商向他们发送新杂志时随时离开该列表。

05 结构实现

在这个例子中,观察者模式让文本编辑器对象通知其他服务对象其状态的变化。

订阅者列表是动态编译的:对象可以在运行时开始或停止监听通知,具体取决于您的应用所需的行为。

在这个实现中,编辑器类本身并不维护订阅列表。它将这项工作委托给专门用于此的特殊帮助对象。您可以将该对象升级为集中式事件调度程序,让任何对象充当发布者。

向程序添加新的订阅者不需要更改现有的发布者类,只要它们通过相同的接口与所有订阅者一起工作。

// The base publisher class includes subscription management
// code and notification methods.
class EventManager is
    private field listeners: hash map of event types and listeners

    method subscribe(eventType, listener) is
        listeners.add(eventType, listener)

    method unsubscribe(eventType, listener) is
        listeners.remove(eventType, listener)

    method notify(eventType, data) is
        foreach (listener in listeners.of(eventType)) do
            listener.update(data)

// The concrete publisher contains real business logic that's
// interesting for some subscribers. We could derive this class
// from the base publisher, but that isn't always possible in
// real life because the concrete publisher might already be a
// subclass. In this case, you can patch the subscription logic
// in with composition, as we did here.
class Editor is
    public field events: EventManager
    private field file: File

    constructor Editor() is
        events = new EventManager()

    // Methods of business logic can notify subscribers about
    // changes.
    method openFile(path) is
        this.file = new File(path)
        events.notify("open", file.name)

    method saveFile() is
        file.write()
        events.notify("save", file.name)

    // ...


// Here's the subscriber interface. If your programming language
// supports functional types, you can replace the whole
// subscriber hierarchy with a set of functions.
interface EventListener is
    method update(filename)

// Concrete subscribers react to updates issued by the publisher
// they are attached to.
class LoggingListener implements EventListener is
    private field log: File
    private field message: string

    constructor LoggingListener(log_filename, message) is
        this.log = new File(log_filename)
        this.message = message

    method update(filename) is
        log.write(replace('%s',filename,message))

class EmailAlertsListener implements EventListener is
    private field email: string
    private field message: string

    constructor EmailAlertsListener(email, message) is
        this.email = email
        this.message = message

    method update(filename) is
        system.email(email, replace('%s',filename,message))


// An application can configure publishers and subscribers at
// runtime.
class Application is
    method config() is
        editor = new Editor()

        logger = new LoggingListener(
            "/path/to/log.txt",
            "Someone has opened the file: %s")
        editor.events.subscribe("open", logger)

        emailAlerts = new EmailAlertsListener(
            "admin@example.com",
            "Someone has changed the file: %s")
        editor.events.subscribe("save", emailAlerts)

06 适用场景

  • 当对一个对象的状态进行更改可能需要更改其他对象时,使用观察者模式,而实际的对象集是事先未知的或动态变化的。

    在使用图形用户界面的类时,您经常会遇到这个问题。例如,您创建了自定义按钮类,并希望让客户端将一些自定义代码挂钩到您的按钮,以便在用户按下按钮时触发它。

  • 观察者模式允许任何实现订阅者接口的对象订阅发布者对象中的事件通知。您可以将订阅机制添加到您的按钮,让客户端通过自定义订阅者类连接他们的自定义代码。

    当您的应用程序中的某些对象必须观察其他对象时使用该模式,但仅限于有限的时间或特定情况。

  • 订阅列表是动态的,因此订阅者可以在需要时加入或离开列表。

07 如何实施

  1. 查看您的业务逻辑并尝试将其分解为两部分:独立于其他代码的核心功能将充当发布者;其余的将变成一组订阅者类别。

  2. 声明订阅者接口。至少,它应该声明一个update方法。

  3. 声明发布者接口并描述一对将订阅者对象添加到列表中和从列表中删除的方法。请记住,发布者必须仅通过订阅者接口与订阅者合作。

  4. 决定将实际订阅列表放在哪里以及订阅方法的实现。通常,对于所有类型的发布者,此代码看起来都相同,因此将其放置在直接从发布者接口派生的抽象类中是显而易见的。具体的发布者扩展了该类,继承了订阅行为。

    但是,如果您将该模式应用于现有的类层次结构,请考虑一种基于组合的方法:将订阅逻辑放入一个单独的对象中,并让所有真正的发布者都使用它。

  5. 创建具体的发布者类。每次发布者内部发生重要事件时,它都必须通知其所有订阅者。

  6. 在具体的订阅者类中实现更新通知方法。大多数订阅者需要一些关于事件的上下文数据。它可以作为通知方法的参数传递。

    但还有另一种选择。收到通知后,订阅者可以直接从通知中获取任何数据。在这种情况下,发布者必须通过更新方法传递自己。不太灵活的选项是通过构造函数将发布者永久链接到订阅者。

  7. 客户端必须创建所有必要的订阅者并将它们注册到适当的发布者。

08 优缺点


欢迎查看公众号内容:
https://mp.weixin.qq.com/s?__biz=Mzk0NjI5NzE1Ng==&mid=2247483755&idx=1&sn=895bd45300754bdd04665cd74f86c708&chksm=c3090140f47e885652dd88d4b3f5ba774a32e4129987fbe2a03bc543e161acd49f68d2f53987&token=787431505&lang=zh_CN#rd

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容