设计模式之观察者(Observer)模式
又名: 事件订阅者(Event-Subscriber), 监听者(Listener)
意图
观察者模式(Observer)是一种行为设计模式,允许你定义一个订阅机制,用以向多个对象通知他们正在观察的对象发生的任何事件。
问题描述
假设你有两类对象: Customer
和 Store
。顾客(customer)对特定品牌的产品非常感兴趣(比如 iphone 的新机型),该产品很快就会在店铺(store)中上线。
顾客可以每天访问店铺并查看产品是否上线。不过,当这些产品仍然在途中的时候,大部分这样的访问变得没有意义。
另一方面,每次有新产品上市,店铺都会向所有顾客发送大量电子邮件(可能被视为垃圾邮件)。这将使一些顾客免于无休止地去商店。同时,这也会让其他对新产品不感兴趣的客户感到不安。
看起来我们有冲突。要么是顾客浪费时间检查产品是否上线,要么是商店浪费资源通知错误的顾客。
方案
这些具有某种有趣状态的对象通常被称为 subject,不过由于它会将其改变的状态通知其他对象,我们称之为发布者(publisher)。所有其他想要跟踪 publisher 状态修改的对象,称为订阅者(subscriber)。
观察者模式建议你添加订阅机制到发布者类,这样单个对象可以从发布者的事件流中订阅和或者取消订阅。不要害怕!一切并不像听起来那么复杂。事实上,这种机制由 1)一个数组字段和 2) 几个公共方法组成,前者用于存储对订阅者对象的引用列表,后者允许向该列表添加订阅者和从该列表中删除订阅者。
现在,每当发布者发生重要事件时,它都会遍历其订阅者,并对其对象调用特定的通知方法。
真正的应用程序可能有几十个不同的订阅者类,它们对跟踪同一发布者类的事件感兴趣。你不会想将发布者与所有这些类结合起来。此外,如果你的发布者类被其他人使用,你甚至可能事先不知道其中的一些。
这就是为什么至关重要的是,所有订阅者都要实现相同的接口,并且发布者只能通过该接口与他们通信。这个接口应该声明通知方法和一组参数,发布者可以使用这些参数来传递一些上下文数据和通知。
如果应用有多个不同类型的发布者并且你想让订阅者兼容,你可以进一步让发布者也遵循同一个接口。这个接口只需要描述一些订阅方法。该接口将允许订阅者在不耦合其实体类的情况下,观察发布者的状态。
真实世界类比
如果你订阅了报纸或杂志,你就不需要再去商店检查下一期是否有货了。相反,出版商会在发布后立即甚至提前将新的报纸直接发送到您的邮箱。
出版商保留一份订阅者名单,并知道他们对哪些杂志感兴趣。当订阅者希望让出版商停止向他们发送新杂志时,可以随时离开名单。
结构
- 发布者发布其他对象感兴趣的事件。事件在发布者改变状态或者执行某些行为时发生。发布者包含一个订阅基础结构,允许新的订阅者加入、已订阅者离开列表。
- 当新事件发生时,发布者遍历订阅者列表并调用在每个订阅对象的订阅接口中声明的通知方法。
- 订阅者接口声明了通知接口。大部分情况下,它只包含单个
update
方法。该方法可能有多个参数,允许发布者在更新的同时传递一些事件详情。 - 具体订阅者(Concrete Subscriber)执行一些操作以响应发布者发出的通知。所有这些类都必须实现相同的接口,这样发布者就不会耦合到具体的类。
- 通常订阅者需要一些上下文信息,以正确处理更新。因此,发布者经常传递一些上下文数作为通知方法的参数。发布者可以传递自己作为参数,让ing约着直接获取需要的数据。
- 客户端(Client)分别创建发布者和订阅者对象,并为为发布者注册订阅者更新。
伪代码
本例中,观察者模式允许文本编辑器对象将其修改状态通知给其他服务对象。
订阅者列表是动态编译的:对象可以在运行时开始或停止监听通知,这取决于应用程序的所需行为。
在这个实现中,编辑器类不单独维护订阅列表。它将此作业委托给专门用于此作业的特殊辅助对象。你可以将该对象升级为集中式事件调度程序,让任何对象都充当发布者。
向程序中添加新的订阅者不需要更改现有的发布者类,只要它们通过相同的接口与所有订阅者一起工作即可。
/ 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)
适用
当一个对象的状态更改可能需要更改其他对象,并且实际的对象集事先未知或动态更改时,可以使用观察者模式。
在使用图形用户界面的类时,你经常会遇到这个问题。例如,你创建了自定义按钮类,并且希望让客户端将一些自定义代码挂接到按钮上,以便每当用户按下按钮时就会触发它。
观察者模式允许任何实现订阅者接口的对象订阅发布者对象中的事件通知。你可以将订阅机制添加到按钮中,让客户端通过自定义订阅者类连接其自定义代码。
当应用程序中的某些对象必须观察其他对象时,但只能在有限的时间内或在特定情况下使用该模式。
订阅列表时动态的,因此订阅者可以随时加入或者离开该列表。
如何实现
- 仔细查看业务逻辑,并尝试将其分解为两部分:独立于其他代码的核心功能将充当发布者;剩下的将变成一组订阅者类。
- 声明订阅者接口。至少,它应该声明一个
update
方法。 - 声明发布者接口,并描述一对用于向列表中添加订阅者对象和从列表中删除订阅者对象的方法。请记住,发布者必须仅通过订阅者接口与订阅者合作。
- 决定将实际订阅列表和订阅方法的实现放在哪里。通常,对于所有类型的发布者,此代码看起来都是相同的,因此显而易见的是,将其放在直接从发布者接口派生的抽象类中。具体的发布者扩展该类,继承订阅行为。
- 不过,如果您该模式应用于现有的类层次结构,请考虑一种基于组合的方法:将订阅逻辑放入一个单独的对象中,并让所有真正的发布者都使用它。
- 创建具体的发布者类。每次发布者内部发生重要事件时,它都必须通知所有订阅者。
- 在具体的订阅者类中实现更新通知方法。大多数订阅者都需要一些有关事件的上下文数据。它可以作为通知方法的参数传递。
- 但还有另一种选择。一旦接收到通知,订阅者就可以直接从通知中获取任何数据。在这种情况下,发布者必须通过 update 方法传递自己。不太灵活的选择是通过构造函数将发布者永久链接到订阅者。
- 客户端必须创建所有必要的订阅者,并向适当的发布者注册。
优缺点
- ✔️开闭原则. 您可以引入新的订阅者类,而无需更改发布者的代码(如果有发布者接口,则反之亦然)。
- ✔️可以在运行时在对象之间建立联系。
- ❌以随机顺序通知订阅者。
与其他模式的关联
- 责任链模式、命令模式、中介者模式和观察者模式解决了连接请求发送者和接收者的各种方式:
- 责任链模式(Chain of Responsibility)沿着潜在接收者的动态链顺序传递请求,直至其中一个接收者处理它。
- 命令模式(Command)在发送者和接收者之间建立单向连接。
- 中介者模式(Mediator)消除了发送者和接收者之间的直接连接,迫使它们通过中介对象进行间接通信。
- 观察者模式(Observer)允许接收机动态订阅,取消订阅解释请求。
- 责任链模式下的 Handler 可以实现为命令。这种情况下,你可以在请求表示的同一个上下文对象中执行许多不同的操作。
- 中介者模式(Mediator)和观察者模式(Observer)之间的区别往往难以捉摸。在大多数情况下,你可以实现这两种模式中的任何一种;但有时你可以同时应用这两种方法。让我们看看如何做到这一点。
Mediator 的主要目标是消除一组系统组件之间的相互依赖关系。相反,这些组件将依赖于单个中介对象。Observer 的目标是在对象之间建立动态的单向连接,其中一些对象充当其他对象的下属。
Mediator 模式有一种流行的实现,它依赖于 Observer。mediator 对象扮演发布者的角色,组件充当订阅者,订阅和取消订阅中介的事件。当以这种方式实现 Mediator 时,它可能看起来与 Observer 非常相似。
当你觉得困惑时,记得用其他方式实现 Mediator 模式。例如,可以将所有组件永久链接到同一个中介对象。这个实现不会类似于 Observer,但仍然是 Mediator 模式的一个实例。
现在想象一个程序,其中所有组件都已成为发布者,允许彼此之间的动态连接。不会有一个中心化的中介对象,只有一组分布式的观察者。
代码示例
index.php: 概念示例
<?php
namespace RefactoringGuru\Command\Conceptual;
/**
* The Command interface declares a method for executing a command.
*/
interface Command
{
public function execute(): void;
}
/**
* Some commands can implement simple operations on their own.
*/
class SimpleCommand implements Command
{
private $payload;
public function __construct(string $payload)
{
$this->payload = $payload;
}
public function execute(): void
{
echo "SimpleCommand: See, I can do simple things like printing (" . $this->payload . ")\n";
}
}
/**
* However, some commands can delegate more complex operations to other objects,
* called "receivers."
*/
class ComplexCommand implements Command
{
/**
* @var Receiver
*/
private $receiver;
/**
* Context data, required for launching the receiver's methods.
*/
private $a;
private $b;
/**
* Complex commands can accept one or several receiver objects along with
* any context data via the constructor.
*/
public function __construct(Receiver $receiver, string $a, string $b)
{
$this->receiver = $receiver;
$this->a = $a;
$this->b = $b;
}
/**
* Commands can delegate to any methods of a receiver.
*/
public function execute(): void
{
echo "ComplexCommand: Complex stuff should be done by a receiver object.\n";
$this->receiver->doSomething($this->a);
$this->receiver->doSomethingElse($this->b);
}
}
/**
* The Receiver classes contain some important business logic. They know how to
* perform all kinds of operations, associated with carrying out a request. In
* fact, any class may serve as a Receiver.
*/
class Receiver
{
public function doSomething(string $a): void
{
echo "Receiver: Working on (" . $a . ".)\n";
}
public function doSomethingElse(string $b): void
{
echo "Receiver: Also working on (" . $b . ".)\n";
}
}
/**
* The Invoker is associated with one or several commands. It sends a request to
* the command.
*/
class Invoker
{
/**
* @var Command
*/
private $onStart;
/**
* @var Command
*/
private $onFinish;
/**
* Initialize commands.
*/
public function setOnStart(Command $command): void
{
$this->onStart = $command;
}
public function setOnFinish(Command $command): void
{
$this->onFinish = $command;
}
/**
* The Invoker does not depend on concrete command or receiver classes. The
* Invoker passes a request to a receiver indirectly, by executing a
* command.
*/
public function doSomethingImportant(): void
{
echo "Invoker: Does anybody want something done before I begin?\n";
if ($this->onStart instanceof Command) {
$this->onStart->execute();
}
echo "Invoker: ...doing something really important...\n";
echo "Invoker: Does anybody want something done after I finish?\n";
if ($this->onFinish instanceof Command) {
$this->onFinish->execute();
}
}
}
/**
* The client code can parameterize an invoker with any commands.
*/
$invoker = new Invoker();
$invoker->setOnStart(new SimpleCommand("Say Hi!"));
$receiver = new Receiver();
$invoker->setOnFinish(new ComplexCommand($receiver, "Send email", "Save report"));
$invoker->doSomethingImportant();
Output.txt: 输出结果
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)