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 如何实施
查看您的业务逻辑并尝试将其分解为两部分:独立于其他代码的核心功能将充当发布者;其余的将变成一组订阅者类别。
声明订阅者接口。至少,它应该声明一个
update
方法。声明发布者接口并描述一对将订阅者对象添加到列表中和从列表中删除的方法。请记住,发布者必须仅通过订阅者接口与订阅者合作。
-
决定将实际订阅列表放在哪里以及订阅方法的实现。通常,对于所有类型的发布者,此代码看起来都相同,因此将其放置在直接从发布者接口派生的抽象类中是显而易见的。具体的发布者扩展了该类,继承了订阅行为。
但是,如果您将该模式应用于现有的类层次结构,请考虑一种基于组合的方法:将订阅逻辑放入一个单独的对象中,并让所有真正的发布者都使用它。
创建具体的发布者类。每次发布者内部发生重要事件时,它都必须通知其所有订阅者。
-
在具体的订阅者类中实现更新通知方法。大多数订阅者需要一些关于事件的上下文数据。它可以作为通知方法的参数传递。
但还有另一种选择。收到通知后,订阅者可以直接从通知中获取任何数据。在这种情况下,发布者必须通过更新方法传递自己。不太灵活的选项是通过构造函数将发布者永久链接到订阅者。
客户端必须创建所有必要的订阅者并将它们注册到适当的发布者。
08 优缺点