观察者模式:订阅你关心的一切
你有没有订阅过某个 UP 主的更新?一旦他发布新视频,你就会收到通知。你不需要每分钟刷新一次页面,系统会"推送"给你。这就是观察者模式的精髓——当感兴趣的事情发生时,主动通知你。
观察者模式 定义了一种一对多的依赖关系:当一个对象状态改变时,所有依赖它的对象都会自动收到通知并更新。
为什么需要观察者模式?
假设你在开发一个电商网站,用户可以订阅商品到货通知:
// ❌ 糟糕的写法:轮询检查
func checkStock() {
for {
if product.InStock {
notifyUser1()
notifyUser2()
notifyUser3()
// ... 每新增一个用户就要改代码
}
time.Sleep(time.Minute)
}
}
这样写的问题:
- 浪费资源 :大部分时间商品都没到货,但程序一直在检查
- 紧耦合 :每新增一个订阅者,都要修改核心代码
- 不灵活 :无法动态添加或移除订阅者
观察者模式的解法: 让商品(发布者)维护一个订阅者列表,到货时主动通知所有人。
模式结构
| 角色 | 职责 | 类比 |
|---|---|---|
| Subject(发布者) | 维护订阅者列表,发送通知 | B站 UP 主 |
| Observer(观察者接口) | 定义接收通知的方法 | 观众的"接收推送"能力 |
| ConcreteObserver(具体观察者) | 接收通知并做出响应 | 具体的粉丝 |
| Client(客户端) | 创建发布者和观察者,建立订阅关系 | 用户点击"关注"按钮 |
动手实现:商品到货通知系统
用电商商品到货通知来演示观察者模式。
第一步:定义观察者接口
第二步:定义发布者接口
第三步:实现具体观察者(顾客)
第四步:实现具体发布者(商品)
package main
import "fmt"
// Item 商品,作为发布者
type Item struct {
observerList []Observer
name string
inStock bool
}
// NewItem 创建商品
func NewItem(name string) *Item {
return &Item{name: name}
}
// Register 添加订阅者
func (i *Item) Register(o Observer) {
i.observerList = append(i.observerList, o)
fmt.Printf("✅ %s 已订阅「%s」的到货通知\n", o.GetID(), i.name)
}
// Deregister 移除订阅者
func (i *Item) Deregister(o Observer) {
for idx, observer := range i.observerList {
if observer.GetID() == o.GetID() {
i.observerList = append(i.observerList[:idx], i.observerList[idx+1:]...)
fmt.Printf("❌ %s 已取消订阅「%s」\n", o.GetID(), i.name)
return
}
}
}
// NotifyAll 通知所有订阅者
func (i *Item) NotifyAll() {
for _, observer := range i.observerList {
observer.Update(i.name)
}
}
// Restock 商品到货
func (i *Item) Restock() {
fmt.Printf("\n🎉 「%s」已到货!正在通知所有订阅者...\n", i.name)
i.inStock = true
i.NotifyAll()
}
第五步:组装并使用
package main
func main() {
// 1. 创建商品(发布者)
shirt := NewItem("Nike 限量款 T恤")
// 2. 创建顾客(观察者)
customer1 := &Customer{email: "zhangsan@example.com"}
customer2 := &Customer{email: "lisi@example.com"}
customer3 := &Customer{email: "wangwu@example.com"}
// 3. 顾客订阅商品到货通知
shirt.Register(customer1)
shirt.Register(customer2)
shirt.Register(customer3)
// 4. 商品到货,自动通知所有订阅者
shirt.Restock()
}
✅ zhangsan@example.com 已订阅「Nike 限量款 T恤」的到货通知
✅ lisi@example.com 已订阅「Nike 限量款 T恤」的到货通知
✅ wangwu@example.com 已订阅「Nike 限量款 T恤」的到货通知
🎉 「Nike 限量款 T恤」已到货!正在通知所有订阅者...
📧 发送邮件给 zhangsan@example.com:您关注的「Nike 限量款 T恤」已到货!
📧 发送邮件给 lisi@example.com:您关注的「Nike 限量款 T恤」已到货!
📧 发送邮件给 wangwu@example.com:您关注的「Nike 限量款 T恤」已到货!
观察者模式的变体
推模型 vs 拉模型
- 推模型:发布者主动把数据推给观察者(上面的例子)
- 拉模型:发布者只通知"有变化",观察者自己去获取数据
// 拉模型:观察者自己获取数据
func (c *Customer) Update(subject Subject) {
item := subject.(*Item)
if item.inStock {
fmt.Printf("我来看看 %s 是什么...\n", item.name)
}
}
事件驱动架构
现代框架通常用"事件"来实现观察者模式:
// 事件总线
eventBus.Subscribe("item.restocked", func(event Event) {
fmt.Println("收到事件:", event.Data)
})
eventBus.Publish("item.restocked", itemData)
什么时候该用观察者模式?
| 场景 | 说明 |
|---|---|
| 一对多依赖关系 | 一个对象变化需要通知多个对象 |
| 松耦合通信 | 发布者和订阅者互不知道对方的具体类型 |
| 动态订阅 | 订阅关系可以在运行时添加或移除 |
| 事件驱动系统 | GUI 事件、消息队列、WebSocket 推送 |
常见应用 :
- GUI 框架:按钮点击事件、输入框变化
- 消息队列:RabbitMQ、Kafka
- 前端框架:Vue 的响应式系统、Redux 的状态订阅
- 社交平台:关注/粉丝系统
优缺点分析
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 开闭原则:新增订阅者无需修改发布者 | 通知顺序不确定:无法控制谁先收到 |
| 运行时建立关系:动态添加/移除订阅 | 可能导致循环依赖:A 通知 B,B 又通知 A |
| 松耦合:发布者和订阅者互不依赖 | 内存泄漏风险:忘记取消订阅会导致对象无法回收 |
与其他模式的关系
| 模式对比 | 说明 |
|---|---|
| 观察者 vs 中介者 | 观察者是分布式通信,中介者是集中式通信 |
| 观察者 vs 发布-订阅 | 发布-订阅有消息代理,更解耦 |
| 观察者 + 责任链 | 事件先经过责任链过滤,再通知观察者 |
一句话总结:观察者模式就像微信公众号——你关注了,就能收到推送;取消关注,就不再收到。公众号不需要知道你是谁,只管发文章。
