观察者模式

观察者模式是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。

../../_images/observer.png

问题

假如你有两种类型的对象: 顾客商店 。 顾客对某个特定品牌的产品非常感兴趣 (例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售。

顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。

../../_images/observer-comic.png

另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。

我们似乎遇到了一个矛盾: 要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。

解决方案

拥有一些值得关注的状态的对象通常被称为目标, 由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者 (publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者 (subscribers)。

观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者事件流。不要害怕! 这并不像听上去那么复杂。 实际上, 该机制包括 :

  • 一个用于存储订阅者对象引用的列表成员变量
  • 几个用于添加或删除该列表中订阅者的公有方法

../../_images/observer_solution1.png

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

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。

因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。

../../_images/observer_solution2.png

如果你的应用中有多个不同类型的发布者, 且希望订阅者可兼容所有发布者, 那么你甚至可以进一步让所有订阅者遵循同样的接口。 该接口仅需描述几个订阅方法即可。 这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态。

观察者模式结构

../../_images/observer_structure.png

  • 发布者 (Publisher) 会向其他对象发送值得关注的事件。 事件会在发布者自身状态改变或执行特定行为后发生。 发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。
  • 当新事件发生时, 发送者会遍历订阅列表并调用每个订阅者对象的通知方法。 该方法是在订阅者接口中声明的。
  • 订阅者 (Subscriber) 接口声明了通知接口。 在绝大多数情况下, 该接口仅包含一个 update更新方法。该方法可以拥有多个参数, 使发布者能在更新时传递事件的详细信息。
  • 具体订阅者 (Concrete Subscribers)可以执行一些操作来回应发布者的通知。 所有具体订阅者类都实现了同样的接口, 因此发布者不需要与具体类相耦合。
  • 订阅者通常需要一些上下文信息来正确地处理更新。 因此, 发布者通常会将一些上下文数据作为通知方法的参数进行传递。 发布者也可将自身作为参数进行传递, 使订阅者直接获取所需的数据。
  • 客户端 (Client) 会分别创建发布者和订阅者对象, 然后为订阅者注册发布者更新。

真实世界类比

../../_images/observer_real_world.png

如果你订阅了一份杂志或报纸, 那就不需要再去报摊查询新出版的刊物了。 出版社 (即应用中的 “发布者”) 会在刊物出版后 (甚至提前) 直接将最新一期寄送至你的邮箱中。

出版社负责维护订阅者列表, 了解订阅者对哪些刊物感兴趣。 当订阅者希望出版社停止寄送新一期的杂志时, 他们可随时从该列表中退出。

代码示例

//@ 抽象观察者
class IObserver
{
public:
	virtual void update(float price) = 0;  //@ 更新价格
};

//@ 具体观察者
class ConcreteObserver : public IObserver
{
public:
	ConcreteObserver(string name) : strName_(name) {}

	void update(float price) {
		cout << strName_ << " - price: " << price << "\n";
	}

private:
	string strName_;  // 名字
};

//@ 抽象主题
class ISubject
{
public:
	virtual void attach(std::shared_ptr<IObserver>) = 0;  //@ 注册观察者
	virtual void detach(std::shared_ptr<IObserver>) = 0;  //@ 注销观察者
	virtual void notify() = 0;  //@ 通知观察者
};

//@ 具体主题
class ConcreteSubject : public ISubject
{
public:
	ConcreteSubject() : fPrice_(10.0) {}

	void setPrice(float price) {
		fPrice_ = price;
	}

	void attach(std::shared_ptr<IObserver> observer) {
		observers_.push_back(observer);
	}

	void detach(std::shared_ptr<IObserver> observer) {
		observers_.remove(observer);
	}

	void notify() {
		list<std::shared_ptr<IObserver>>::iterator it = observers_.begin();
		while (it != observers_.end()) {
			(*it)->update(fPrice_);
			++it;
		}
	}

private:
	list<std::shared_ptr<IObserver>> observers_;  // 观察者列表
	float fPrice_;  // 价格
};

int main()
{
	//@ 创建主题、观察者
	std::shared_ptr<ConcreteSubject> pSubject(new ConcreteSubject());
	std::shared_ptr<IObserver> pObserver1(new ConcreteObserver("Jack Ma"));
	std::shared_ptr<IObserver> pObserver2(new ConcreteObserver("Pony"));

	//@ 注册观察者
	pSubject->attach(pObserver1);
	pSubject->attach(pObserver2);

	//@ 更改价格,并通知观察者
	pSubject->setPrice(12.5);
	pSubject->notify();

	//@ 注销观察者
	pSubject->detach(pObserver2);
	//@ 再次更改状态,并通知观察者
	pSubject->setPrice(15.0);
	pSubject->notify();

	return 0;
}

实现方式

  • 仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类。
  • 声明订阅者接口。 该接口至少应声明一个 update方法。
  • 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。 记住发布者必须仅通过订阅者接口与它们进行交互。
  • 确定存放实际订阅列表的位置并实现订阅方法。 通常所有类型的发布者代码看上去都一样, 因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。 具体发布者会扩展该类从而继承所有的订阅行为。
    • 但是, 如果你需要在现有的类层次结构中应用该模式, 则可以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。
  • 创建具体发布者类。 每次发布者发生了重要事件时都必须通知所有的订阅者。
  • 在具体订阅者类中实现通知更新的方法。 绝大部分订阅者需要一些与事件相关的上下文数据。 这些数据可作为通知方法的参数来传递。
    • 但还有另一种选择。 订阅者接收到通知后直接从通知中获取所有数据。 在这种情况下,发布者必须通过更新方法将自身传递出去。 另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。
  • 客户端必须生成所需的全部订阅者, 并在相应的发布者处完成注册工作。

观察者模式总结

优点

  • 开闭原则。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。
  • 你可以在运行时建立对象之间的联系。

缺点

  • 订阅者的通知顺序是随机的。

适用场景

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • 一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。
  • 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。