观察者模式原理介绍
定义
观察者模式(Observer Pattern),又称为发布-订阅模式(Publish-Subscribe Pattern),是一种行为设计模式,用于在对象之间建立一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,所有依赖于它的观察者都会得到通知并自动更新。
适用场景
- 当一个对象的改变需要同时改变其他对象,并且这些对象可能还未知或者尚未创建时。
- 当对象间的耦合关系过于紧密,需要降低耦合度时。
多对一的订阅/发布关系
在观察者模式中,存在两种类型的对象:观察者(Observer)和主题(Subject)。主题是被观察的对象,而观察者是依赖于主题的对象。一个主题可以有多个观察者,但观察者对主题的了解是抽象的,不依赖于主题的具体实现。
观察者与被观察对象之间的解耦
观察者模式的核心优势在于解耦。通过引入一个中介(通常称为事件总线 EventBus),观察者不必直接与主题交互,而是通过事件总线进行通信。这样,当主题的状态发生变化时,事件总线负责通知所有注册的观察者,而观察者只需要关心自己的更新逻辑。
UML图
流程图
代码示例
type Observer interface {
Onchange(ctx context.Context, e Event) error
}
type EventBus interface {
Subscribe(topic string, o Observer)
Unsubscribe(topic string, o Observer)
Publish(ctx context.Context, e *Event)
}
// 具体实现
type BaseObserver struct {
name string
}
func (o *BaseObserver) OnChange(ctx context.Context, e Event) error {
fmt.Printf("Observer %s received update: %s\n", o.name, e.Topic)
return nil
}
type BaseEventBus struct {
observers map[string][]Observer
}
func (bus *BaseEventBus) Subscribe(topic string, o Observer) {
// Subscribe logic
}
func (bus *BaseEventBus) Unsubscribe(topic string, o Observer) {
// Unsubscribe logic
}
func (bus *BaseEventBus) Publish(ctx context.Context, e *Event) {
// Publish logic
}
观察者模式核心角色
Observer(观察者角色)
观察者是模式中的一个抽象概念,它定义了对主题对象感兴趣的对象需要实现的接口。在Golang中,通常是一个接口,包含一个或多个方法,用于接收来自主题的通知。
Event(事件角色)
事件是主题状态改变的具体信息,通常是一个结构体,包含有关事件的详细信息,如事件类型、事件数据等。
EventBus(事件总线,代理层)
事件总线充当观察者和主题之间的中介,负责管理观察者列表、订阅和取消订阅操作,以及在主题状态变化时通知所有注册的观察者。
UML图
角色职责
- Observer: 观察者需要实现
OnChange
方法,用于接收来自EventBus
的通知,并根据事件类型和数据执行相应的逻辑。 - Event: 事件对象通常包含主题状态改变的详细信息,如主题的标识、变更的数据等。
- EventBus: 事件总线负责维护观察者列表,提供订阅和取消订阅的方法,以及在事件发生时通知所有相关的观察者。
代码示例
// Observer接口定义
type Observer interface {
OnChange(ctx context.Context, event *Event) error
}
// Event结构定义
type Event struct {
Topic string // 事件的主题
Data interface{} // 事件携带的数据
}
// EventBus接口定义
type EventBus interface {
Subscribe(topic string, o Observer)
Unsubscribe(topic string, o Observer)
Publish(ctx context.Context, event *Event)
}
// 具体实现
type BaseEventBus struct {
observers map[string][]Observer
// 其他成员变量和方法
}
func (bus *BaseEventBus) Subscribe(topic string, o Observer) {
// 订阅逻辑
}
func (bus *BaseEventBus) Unsubscribe(topic string, o Observer) {
// 取消订阅逻辑
}
func (bus *BaseEventBus) Publish(ctx context.Context, event *Event) {
// 发布逻辑,遍历所有订阅者并调用其OnChange方法
for _, observer := range bus.observers[event.Topic] {
if err := observer.OnChange(ctx, event); err != nil {
// 错误处理
}
}
}
通过上述图文结合的剖析,我们可以清楚地看到观察者模式中每个核心角色的职责和它们之间的交互方式。这种模式提高了系统的灵活性和可扩展性,使得在主题对象状态变化时,所有相关的观察者都能得到通知并作出响应。
通过这个图文结合的剖析,我们可以更清晰地理解观察者模式的工作原理和实现方式。
观察者与事件总线的实现
BaseObserver 的实现
BaseObserver
是一个具体的观察者实现,它实现了 Observer
接口。这个基础观察者可以被其他具体的观察者继承或直接使用,以便于接收事件通知并作出响应。
BaseEventBus 的实现
BaseEventBus
是事件总线的实现,它包含了管理订阅者和分发事件的逻辑。它允许观察者订阅和取消订阅特定的事件主题,并在事件发生时通知所有订阅者。
Subscribe 和 Unsubscribe 方法
Subscribe
方法允许观察者注册自己对某个事件主题的兴趣。Unsubscribe
方法允许观察者取消对某个事件主题的兴趣。
事件通知的两种模式
同步模式
- 在同步模式下,
EventBus
在接收到事件时,会同步地遍历所有订阅者,并调用它们的OnChange
方法。 - 这种方式保证了事件的顺序性,但可能因为某个观察者的长时间处理而影响其他观察者的响应。
异步模式
- 在异步模式下,
EventBus
会启动一个或多个协程来处理事件的分发,每个订阅者在单独的协程中被通知。 - 这种方式提高了事件处理的并发性,可以避免单个观察者的长时间处理影响整个系统的响应性。
UML图
代码示例
// BaseObserver 接口实现
type BaseObserver struct {
name string
}
func (o *BaseObserver) OnChange(ctx context.Context, event *Event) error {
fmt.Printf("Observer %s received event: %s with data %v\n", o.name, event.Topic, event.Data)
return nil
}
// BaseEventBus 实现
type BaseEventBus struct {
sync.RWMutex
observers map[string][]Observer
}
func (bus *BaseEventBus) Subscribe(topic string, observer BaseObserver) {
bus.Lock()
bus.observers[topic] = append(bus.observers[topic], observer)
bus.Unlock()
}
func (bus *BaseEventBus) Unsubscribe(topic string, observer BaseObserver) {
bus.Lock()
for i, o := range bus.observers[topic] {
if o == observer {
bus.observers[topic] = append(bus.observers[topic][:i], bus.observers[topic][i+1:]...)
break
}
}
bus.Unlock()
}
func (bus *BaseEventBus) Publish(ctx context.Context, event *Event) {
bus.RLock()
subscribers := bus.observers[event.Topic]
bus.RUnlock()
for _, subscriber := range subscribers {
subscriber.OnChange(ctx, event)
}
}
// SyncEventBus 和 AsyncEventBus 继承 BaseEventBus 并实现各自的 Publish 方法
通过上述图文结合的剖析,我们可以清楚地理解观察者与事件总线的实现方式,以及同步和异步事件通知模式的特点和区别。
同步模式
SyncEventBus 的定义与实现
同步模式下的 SyncEventBus
是 BaseEventBus
的一个具体实现,它在接收到主题对象状态变化时,会按照订阅的顺序,一个接一个地通知所有观察者。这种模式保证了事件处理的顺序性,但可能因为单个观察者的长时间处理而阻塞其他观察者的处理。
串行通知观察者
在同步模式中,SyncEventBus
通过串行地调用每个观察者的 OnChange
方法来通知它们。这意味着,直到当前观察者处理完毕前,下一个观察者不会开始处理。这种方式适用于对事件处理顺序有严格要求的场景。
UML图
代码示例
type SyncEventBus struct {
BaseEventBus
}
func (bus *SyncEventBus) Publish(ctx context.Context, event *Event) {
bus.RLock()
subscribers := bus.observers[event.Topic]
bus.RUnlock()
for _, subscriber := range subscribers {
// 同步调用每个观察者的OnChange方法
subscriber.OnChange(ctx, event)
}
}
流程图
在这个流程图中,我们可以看到事件产生后,SyncEventBus
获取了所有对此事件主题感兴趣的观察者,然后按照订阅的顺序,依次调用每个观察者的 OnChange
方法进行事件处理。这种串行处理方式确保了事件的顺序性,但也可能因为某个观察者的长时间处理而影响整体效率。
特点
-
优点:
- 保证了事件处理的顺序性。
- 简化了事件处理逻辑,因为不需要考虑并发问题。
-
缺点:
- 单个观察者的长时间处理可能会阻塞其他观察者。
- 整体事件处理的速度受限于最慢的观察者。
通过上述图文结合的剖析,我们可以更深入地理解同步模式下事件通知的机制和特点。
异步模式
AsyncEventBus 的定义与实现
异步模式下的 AsyncEventBus
是 BaseEventBus
的另一个具体实现。与同步模式不同,AsyncEventBus
在通知观察者时会采用并发的方式,这样可以提高事件处理的效率,避免单个观察者的处理时间影响整个系统的响应速度。
并发通知观察者
在 AsyncEventBus
中,当一个事件发生时,它会为每个订阅该事件的观察者启动一个独立的协程(goroutine),从而实现并发处理。这种方式允许多个观察者同时接收并处理事件,提高了系统的并行处理能力。
错误处理机制
由于异步模式下观察者在不同的协程中运行,因此需要一种机制来处理可能出现的错误。通常,这些错误会被收集并发送到一个共享的错误通道(channel),然后在一个单独的协程中进行处理,以避免影响其他观察者的正常处理。
UML图
代码示例
type AsyncEventBus struct {
BaseEventBus
errCh chan error
}
func (bus *AsyncEventBus) Publish(ctx context.Context, event *Event) {
bus.RLock()
subscribers := bus.observers[event.Topic]
bus.RUnlock()
for _, subscriber := range subscribers {
go func(subscriber BaseObserver) {
if err := subscriber.OnChange(ctx, event); err != nil {
// 将错误发送到错误通道
bus.errCh <- fmt.Errorf("error in observer: %v", err)
}
}(subscriber)
}
}
func (bus *AsyncEventBus) StartErrorHandling() {
go func() {
for {
select {
case err := <-bus.errCh:
// 处理错误
log.Println(err)
case <-ctx.Done():
return
}
}
}()
}
func (bus *AsyncEventBus) StopErrorHandling() {
close(bus.errCh)
}
使用示例
同步模式的使用示例
func TestSyncEventBus(t *testing.T) {
bus := NewSyncEventBus()
defer bus.Stop()
observerA := NewBaseObserver("A")
observerB := NewBaseObserver("B")
bus.Subscribe("topic1", observerA)
bus.Subscribe("topic1", observerB)
event := &Event{Topic: "topic1", Data: "data"}
bus.Publish(context.Background(), event)
}
异步模式的使用示例
func TestAsyncEventBus(t *testing.T) {
bus := NewAsyncEventBus()
defer bus.Stop()
bus.StartErrorHandling()
defer bus.StopErrorHandling()
observerA := NewBaseObserver("A")
observerB := NewBaseObserver("B")
bus.Subscribe("topic1", observerA)
bus.Subscribe("topic1", observerB)
event := &Event{Topic: "topic1", Data: "data"}
bus.Publish(context.Background(), event)
}
工程案例
在工程实践中,观察者模式可以应用于多种场景,例如:
-
消息队列**(MQ)**:在消息队列系统中,生产者发布消息到队列,而消费者订阅队列并消费消息。这可以看作是一种观察者模式的应用,其中消息队列系统充当
EventBus
,消息是Event
,消费者是Observer
。 -
分布式****配置中心(如ETCD):在分布式系统中,配置信息的变更需要实时通知到所有相关的服务。ETCD 提供了监听机制,当配置发生变化时,它会通知所有订阅该配置的服务。这同样是一种观察者模式的应用,其中配置变更是
Event
,服务是Observer
。
通过这些示例,我们可以看到观察者模式在实际开发中的广泛应用,它提供了一种有效的事件通知和处理机制,有助于构建松耦合和高内聚的系统。
MQ发布/订阅
消息队列中的观察者模式应用
消息队列(MQ)是一种应用程序之间传输消息的中间件,它提供了一种异步的通信机制。在MQ中,观察者模式的应用体现在生产者(Producer)和消费者(Consumer)的交互上。
- 生产者:作为观察者模式中的“主题对象”,负责发布消息到消息队列。
- 消费者:作为“观察者”,订阅感兴趣的消息队列,并在消息到达时接收通知并处理消息。
生产者与消费者的角色对应
- 生产者:生成数据并将其发送到MQ,类似于观察者模式中的Subject,它触发事件。
- 消费者:监听MQ上的特定主题或队列,类似于Observer,它响应事件。
UML图
流程图
ETCD监听回调
分布式KV存储组件中的观察者模式
ETCD是一个分布式键值存储系统,它提供了一种监听机制,允许客户端订阅特定键的变化,并在键值变化时接收回调通知。
- 服务端:作为观察者模式中的“主题对象”,管理键值对并触发变更事件。
- 客户端:作为“观察者”,订阅感兴趣的键,并在键值变化时接收通知。
etcd服务端的watch功能实现
ETCD的watch功能允许客户端设置一个watcher来监听特定键的变更。当键值发生变化时,服务端会通知所有订阅该键的watcher。
UML图
流程图
!
在这两个工程案例中,我们可以看到观察者模式如何被应用于实际的系统设计中,无论是消息队列的发布/订阅机制,还是ETCD的监听回调功能,都体现了观察者模式在实现系统解耦和提高响应性方面的优势。
总结
观察者模式的适用性
观察者模式是一种非常实用的设计模式,它主要用于以下场景:
- 当一个对象的状态变化需要同时影响其他对象时。
- 当对象间的耦合关系过于紧密,需要降低耦合度,提高模块的独立性时。
- 在事件驱动的系统中,需要一种机制来管理和响应事件。
设计模式的实践意义
设计模式是软件设计中的经验和最佳实践的总结,它们提供了解决常见设计问题的模板。使用设计模式有以下好处:
- 提高代码的可读性和可维护性:设计模式提供了一种通用的语言和结构,使得代码更容易被理解和维护。
- 促进重用:设计模式鼓励开发者创建可重用的组件,减少重复代码。
- 解决复杂问题:设计模式提供了解决复杂设计问题的方法,简化了设计过程。
观察者模式的两种通信模式:同步与异步
观察者模式支持两种通信模式,各有其特点和适用场景:
- 同步模式:
- 事件的发送者(主题)在发送事件后会等待所有观察者处理完毕。
- 保证了事件处理的顺序性。
- 适用于对事件处理顺序有严格要求的场景。
- 缺点是如果某个观察者处理时间过长,可能会阻塞其他观察者。
- 异步模式:
- 事件的发送者在发送事件后不会等待观察者处理完毕,可以继续执行其他任务。
- 观察者在接收到事件后并发处理,提高了系统的响应性和并发处理能力。
- 适用于需要快速响应和高并发处理的场景。
- 需要考虑并发控制和错误处理机制。
同步模式与异步模式的对比图
在这个对比图中,我们可以看到同步模式下事件总线会按照顺序通知每个观察者,而异步模式下事件总线会同时通知所有观察者,观察者并发处理事件。
设计模式的实践意义
设计模式的实践意义可以通过以下思维导图来表示:
通过这个思维导图,我们可以清晰地看到设计模式如何帮助我们提高软件设计的质量和效率。
总结来说,观察者模式是一种强大的设计模式,它通过定义对象之间的一对多依赖关系,帮助我们构建松耦合、易于维护和扩展的系统。同步和异步通信模式提供了不同的解决方案,以满足不同场景下的需求。