亦称:事件订阅、监听器、Listener、Observer

观察者模式定义了一种多对一的依赖关系,让多个观察者对象同时监听某一个主题对象。 当主题对象的状态发生变化时,会通知所有观察者对象。

背景

假如有两种类型的对象:顾客和商店。 顾客对某个特定品牌的产品非常感兴趣,而该产品很快将会在商店里出售。

  • 顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时,绝大多数来到商店的顾客都会空手而归。
  • 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。这样,部分顾客就无需反复前往商店了,但也可能会惹恼对新产品没有兴趣的其他顾客。

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

解决方案

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

所有希望关注发布者状态变化的其他对象被称为订阅者 (subscribers)。

观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者的事件流。 该机制包括

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

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

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 我们不希望发布者与所有这些类相耦合的

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

观察者模式

适用场景

  • 当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式。
    • 创建了自定义按钮类并允许客户端在按钮中注入自定义代码, 这样当用户按下按钮时就会触发这些代码。
    • 观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。 你可在按钮中添加订阅机制, 允许客户端通过自定义订阅类注入自定义代码。
  • 当应用中的一些对象必须观察其他对象时, 可使用该模式
    • 订阅列表是动态的, 因此订阅者可随时加入或离开该列表。

实现步骤

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

优缺点

优点:

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

缺点:

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

与其它模式的关系

  • 这几种模式都可用于实现发送者接收者之间的连接方式:
    • 责任链模式按照顺序将请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理。
    • 命令模式在发送者和请求者之间建立单向连接。
    • 中介者模式清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
    • 观察者模式允许接收者动态地订阅或取消接收请求。
  • 中介者观察者之间的区别往往很难理清,可以使用其中一种模式,也可以同时使用。
    • 中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。
    • 观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。
    • 有一种流行的中介者模式实现依赖于观察者:中介者对象担当发布者的角色,其他组件则作为订阅者,可以订阅中介者的事件或取消订阅。
    • 对于中介者模式,可以永久性地将所有组件链接到同一个中介者对象。而观察者模式讲究动态连接。
    • 假设有一个程序,其所有的组件都变成了发布者,它们之间可以相互建立动态连接。这样程序中就没有中心化的中介者对象,而只有一些分布式的观察者。

其它

观察者模式是比较通用的设计模式,思路比较清晰。

  • 发布者维护订阅者集合,当发布者有更新时,通知各个订阅者。
  • 发布者为了管理订阅者集合,提供了增加、删除订阅者的方法。

当发布者有更新,需要通知订阅者时,有很多细节值得讨论。

  1. 通知的方式使用同步还是异步
    • 同步方式实现简单;
    • 异步方式除了能实现代码解耦之外,还能提高代码的执行效率;
  2. 如何保证所有订阅者都通知成功
    • 利用消息队列ACK的能力,订阅者订阅消息队列主题,发布者只需要确保信息提交给消息队列即可。
    • 发布者将失败的通知记录,方便后面进行重试。
    • 定义好规范,例如只对网络延迟这种错误进行记录,而不关注业务操作失败,由业务自行保证成功。
  3. 不同进程/系统如何进行通知
    • 进程间的观察者模式解耦更加彻底,一般是基于消息队列来实现,用来实现不同进程间的被观察者和观察者之间的交互。

样例

subject.go

1
2
3
4
5
6
7
8
9
package main

// Subject 主体接口 发布者
type Subject interface {
	register(observer)
	deregister(observer)
	notifyAll()
}

observer.go

1
2
3
4
5
6
7
8
package main

// observer 观察者接口 订阅者
type observer interface {
	update(string)
	getID() string
}

item.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import "fmt"

// Item 具体主体
type Item struct {
	observerList []observer
	name         string
	inStock      bool
}

func newItem(name string) *Item {
	return &Item{name: name}
}

func (oi *Item) register(obs observer) {
	oi.observerList = append(oi.observerList, obs)
}

func (oi *Item) deregister(obs observer) {
	var list []observer
	var index int
	var o observer

	for index, o = range oi.observerList {
		if o.getID() == obs.getID() {
			break
		}
	}
	list = oi.observerList[:index]
	list = append(list, oi.observerList[index+1:]...)

	oi.observerList = list
}

func (oi *Item) notifyAll() {
	for _, obs := range oi.observerList {
		obs.update(oi.name)
	}
}

func (oi *Item) updateAvailability() {
	fmt.Printf("item %s is now in stock\n", oi.name)
	oi.inStock = true
	oi.notifyAll()
}

customer.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

// Customer 具体观察者
type Customer struct {
	id string
}

func (oc *Customer) getID() string {
	return oc.id
}

func (oc *Customer) update(itemName string) {
	fmt.Printf("Sending email to customer %s for item %s\n", oc.id, itemName)
}

demo.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

func main() {
	shirtItem := newItem("Lining Shirt")

	obsFirst := &Customer{id: "abc@163.com"}
	obsSecond := &Customer{id: "xyz@163.com"}

	// 注册观察者
	shirtItem.register(obsFirst)
	shirtItem.register(obsSecond)

	// 通知观察者
	shirtItem.updateAvailability()
}

References

guru