观察者模式
概述
观察者模式(Observer Pattern)是一种行为型设计模式,定义了对象间一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会收到通知并自动更新。它通过将主题(Subject)和观察者(Observer)解耦,允许动态添加或移除观察者,广泛应用于事件驱动系统。观察者模式遵循“开闭原则”和“依赖倒置原则”,适用于需要动态响应状态变化的场景,如用户界面更新、消息通知系统等。与发布-订阅模式类似,但观察者模式通常更直接,主题直接管理观察者,而发布-订阅模式通过中间件(如消息队列)解耦。
模式结构
观察者模式的主要角色如下:
- 主题接口(Subject):定义注册、移除和通知观察者的接口。
- 具体主题(Concrete Subject):维护观察者列表,管理状态并在状态变化时通知观察者。
- 观察者接口(Observer):定义更新方法,接收主题状态变化的通知。
- 具体观察者(Concrete Observer):实现观察者接口,根据通知更新自身状态。
- 客户端(Client):创建主题和观察者,触发状态变化和通知。
实现
观察者模式的 UML 类图如下所示:
新闻发布示例
subject.go
代码如下:
go
package observer
// 观察者模式
// 主题接口
// Subject 定义主题接口,管理观察者
type Subject interface {
RegisterObserver(o Observer)
RemoveObserver(o Observer)
NotifyObservers()
}
news_agency.go
代码如下:
go
package observer
// 观察者模式
// 具体主题
// NewsAgency 是具体主题,管理新闻和观察者
type NewsAgency struct {
observers []Observer
news string
}
// RegisterObserver 添加观察者
func (n *NewsAgency) RegisterObserver(o Observer) {
n.observers = append(n.observers, o)
}
// RemoveObserver 移除观察者
func (n *NewsAgency) RemoveObserver(o Observer) {
for i, observer := range n.observers {
if observer == o {
n.observers = append(n.observers[:i], n.observers[i+1:]...)
break
}
}
}
// NotifyObservers 通知所有观察者
func (n *NewsAgency) NotifyObservers() {
for _, observer := range n.observers {
observer.Update(n.news)
}
}
// SetNews 设置新闻并通知观察者
func (n *NewsAgency) SetNews(news string) {
n.news = news
n.NotifyObservers()
}
observer.go
代码如下:
go
package observer
// 观察者模式
// 观察者接口
// Observer 定义观察者接口,处理通知
type Observer interface {
Update(news string)
}
news_channel.go
代码如下:
go
package observer
// 观察者模式
// 具体观察者
// NewsChannel 是具体观察者,接收和存储新闻
type NewsChannel struct {
name string
news string
}
// NewNewsChannel 创建具体观察者实例
func NewNewsChannel(name string) *NewsChannel {
return &NewsChannel{name: name}
}
// Update 更新观察者的新闻内容
func (n *NewsChannel) Update(news string) {
n.news = news
}
// GetNews 获取观察者的新闻内容
func (n *NewsChannel) GetNews() string {
return n.news
}
客户端(单元测试)
client_test.go
代码如下:
go
package observer
import (
"testing"
)
// 单元测试
// 模拟客户端调用
// TestObserver 测试观察者模式的场景
func TestObserver(t *testing.T) {
agency := &NewsAgency{}
channel1 := NewNewsChannel("频道1")
channel2 := NewNewsChannel("频道2")
tests := []struct {
name string
news string
expectedNews string
}{
{
name: "发布第一条新闻",
news: "突发新闻:事件A",
expectedNews: "突发新闻:事件A",
},
{
name: "发布第二条新闻",
news: "最新报道:事件B",
expectedNews: "最新报道:事件B",
},
}
// 注册观察者
agency.RegisterObserver(channel1)
agency.RegisterObserver(channel2)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 发布新闻
agency.SetNews(tt.news)
// 验证观察者接收的新闻
if result := channel1.GetNews(); result != tt.expectedNews {
t.Errorf("频道1 期望新闻 %q,实际得到 %q", tt.expectedNews, result)
}
if result := channel2.GetNews(); result != tt.expectedNews {
t.Errorf("频道2 期望新闻 %q,实际得到 %q", tt.expectedNews, result)
}
t.Logf("新闻发布: %s", tt.expectedNews)
})
}
// 测试移除观察者
agency.RemoveObserver(channel2)
agency.SetNews("新事件:事件C")
if channel2.GetNews() != "最新报道:事件B" {
t.Errorf("频道2 不应接收新新闻,期望 %q,实际得到 %q", "最新报道:事件B", channel2.GetNews())
}
if channel1.GetNews() != "新事件:事件C" {
t.Errorf("频道1 期望新闻 %q,实际得到 %q", "新事件:事件C", channel1.GetNews())
}
}
实现说明
观察者模式通过 Subject
接口定义了主题的行为,NewsAgency
(具体主题)维护观察者列表并在新闻更新时通知所有观察者。Observer
接口定义了观察者的更新方法,NewsChannel
(具体观察者)接收并存储新闻内容。客户端通过注册和移除观察者、设置新闻内容来触发通知。测试代码验证了观察者模式的动态注册、通知和移除功能,展示了主题与观察者的解耦以及状态更新的正确性。
优点与缺点
优点:
- 松耦合:主题和观察者解耦,观察者可动态添加或移除。
- 支持广播:一个主题状态变化可通知多个观察者,适合一对多通信。
- 符合开闭原则:易于扩展新的观察者,无需修改主题代码。
- 动态管理:观察者可在运行时注册或取消注册,灵活性高。
- 事件驱动:适合事件驱动系统,如 GUI 事件处理或消息通知。
缺点:
- 内存泄漏风险:未移除的观察者可能导致内存泄漏。
- 通知开销:大量观察者可能导致通知性能下降。
- 复杂性增加:需要维护观察者列表,增加设计和维护成本。
- 通知顺序不定:观察者通知顺序不可控,可能影响某些场景。
- 线程安全问题:并发环境中需确保观察者列表和通知的线程安全。
适用场景
观察者模式适用于以下场景:
- 状态变化通知:一个对象状态变化需通知多个相关对象,如 UI 组件更新。
- 事件驱动系统:需要响应事件或触发回调,如按钮点击、传感器数据更新。
- 消息广播:需要将消息广播给多个订阅者,如新闻发布或消息队列。
- 解耦模块:模块间需松耦合通信,如 MVC 架构中的视图和模型。
- 实时更新:需要实时同步状态,如股票价格监控或聊天应用。
注意事项
- Go 设计哲学:Go 强调简单性和性能,观察者模式适合事件驱动场景,但应避免过度复杂的设计。
- 线程安全:在并发环境中,
NewsAgency
的观察者列表需加锁(如sync.RWMutex
)以确保线程安全。 - 内存管理:及时移除不再需要的观察者,避免内存泄漏。
- 通知优化:若观察者数量多,可考虑异步通知或批量更新以提高性能。
- 与发布-订阅模式区分:观察者模式中主题直接管理观察者,而发布-订阅模式通常通过中间件(如消息代理)解耦。