观察者模式介绍
观察者模式(Observer Design Pattern)定义了一种一对多的依赖关系,让多个观察者对象同时监听一个主题对象。这个主题对象在状态发生变化的时,会通知所有的观察者对象,使他们能够更新自己。
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
代码结构
话不多说,直接上代码
├── README.md
├── consumer
│ └── interface.go // 定义消费者需要实现的接口
├── event
│ └── event.go // 定义事件类型和接口
├── example
│ └── main.go // 测试demo
└── subject
├── interface.go // 定义主题接口
└── subject.go // 实现主题接口
代码解析
consumer需要实现的接口
接口定义
type Consumer interface {
Exec(ctx context.Context, event event.Event) error
Name() string
}
消费者只要实现了这个接口,就可以被主题的AddConsumer
调用,作为观察者去监听主题发布的事件。
Exec(ctx context.Context, event event.Event) error
接口会被主题发布事件的时候调用Name() string
消费者名称,主要用于移除消费者
type ExecFunc func(ctx context.Context, event event.Event) error
func (f ExecFunc) Exec(ctx context.Context, event event.Event) error {
return f(ctx, event)
}
func (f ExecFunc) Name() string {
return fmt.Sprintf("%d", &f)
}
这里还定义了函数类型的消费者type ExecFunc func(ctx context.Context, event event.Event) error
,已经在ExecFunc
函数类型上实现了Consumer的两个接口。所以消费者也可以是一个签名为func(ctx context.Context, event event.Event)
的函数,在调用subject.AddConsumer
的时候只需要将自定义函数转换成ExecFunc
类型即可。
事件类型和接口
type EventType string
type Event interface {
EventType() EventType
EventValue() any
}
定义了事件类型EventType
和接口Event
,自定义事件的时候只需要实现这个接口,例如:
// 定义登录事件类型
var (
LoginEventType event.EventType = "login"
)
// 定义登录事件详情
type LoginEvent struct {
userID string
date time.Time
}
func (l *LoginEvent) EventType() event.EventType {
return LoginEventType
}
func (l *LoginEvent) EventValue() any {
return l
}
主题接口和实现
主题接口
type Subject interface {
AddConsumer(e event.EventType, consumer consumer.Consumer)
RemoveConsumer(e event.EventType, consumer consumer.Consumer)
NotifyConsumer(ctx context.Context, e event.Event)
}
主题即被观察者,被观察者可能有多个事件,所以定义了EventType
主题的实现
type subject struct {
eventConsumers map[event.EventType]map[string]consumer.Consumer
mu sync.Mutex
}
func New() Subject {
return &subject{
eventConsumers: make(map[event.EventType]map[string]consumer.Consumer),
}
}
// 添加观察者
func (s *subject) AddConsumer(e event.EventType, c consumer.Consumer) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.eventConsumers[e]; !ok {
s.eventConsumers[e] = make(map[string]consumer.Consumer)
}
if _, ok := s.eventConsumers[e][c.Name()]; ok {
log.Errorf("consumer:%s exists.", c.Name())
return
}
s.eventConsumers[e][c.Name()] = c
log.Debugf("add consumer:%s", c.Name())
}
// 移除观察者
func (s *subject) RemoveConsumer(e event.EventType, c consumer.Consumer) {
if _, ok := s.eventConsumers[e]; !ok {
s.eventConsumers[e] = make(map[string]consumer.Consumer)
return
}
delete(s.eventConsumers[e], c.Name())
log.Debugf("remove consumer:%s", c.Name())
}
// 通知观察者
func (s *subject) NotifyConsumer(ctx context.Context, e event.Event) {
consumers, ok := s.eventConsumers[e.EventType()]
if !ok {
return
}
for _, c := range consumers {
c.Exec(ctx, e)
}
}
主题的实现看起来代码很多,有60行,但是非常简单。它维护了一个map,保存各种事件的消费者,然后分别实现AddConsumer,RemoveConsumer,NotifyConsumer
方法。
测试
下面是测试代码
func main() {
// 定义一个主题
subject := subject.New()
// 定义用户消费者
u := user("")
// 注册用户监听登录、登出的方法
subject.AddConsumer(LoginEventType, consumer.ExecFunc(u.HandleLogin))
subject.AddConsumer(LogoutEventType, consumer.ExecFunc(u.HandleLogout))
// 定义系统日志消费者
l := logManager("")
// 注册用户监听登录、登出的方法
subject.AddConsumer(LoginEventType, consumer.ExecFunc(l.HandleLogin))
subject.AddConsumer(LogoutEventType, consumer.ExecFunc(l.HandleLogout))
// 创建一个登录事件
inEvent := &LoginEvent{
userID: "abc",
date: time.Now(),
}
ctx := context.Background()
// 通知所有订阅者
subject.NotifyConsumer(ctx, inEvent)
// 创建一个登出事件
outEvent := &LogoutEvent{
userID: "abc",
date: time.Now(),
}
// 通知所有订阅者
subject.NotifyConsumer(ctx, outEvent)
// 移除用户监听登录事件
subject.RemoveConsumer(LoginEventType, consumer.ExecFunc(u.HandleLogin))
// 发送登录通知
subject.NotifyConsumer(ctx, inEvent)
}
// 定义两个事件类型
var (
LoginEventType event.EventType = "login"
LogoutEventType event.EventType = "logout"
)
// 定义事件详情
type LoginEvent struct {
userID string
date time.Time
}
func (l *LoginEvent) EventType() event.EventType {
return LoginEventType
}
func (l *LoginEvent) EventValue() any {
return l
}
type LogoutEvent struct {
userID string
date time.Time
}
func (l *LogoutEvent) EventType() event.EventType {
return LogoutEventType
}
func (l *LogoutEvent) EventValue() any {
return l
}
// 定义用户,需要监听登录、退出的消息
type user string
// 监听登录消息
func (u *user) HandleLogin(ctx context.Context, e event.Event) error {
loginEvent, ok := e.EventValue().(*LoginEvent)
if !ok {
return nil
}
fmt.Printf("you login event at:%v\n", loginEvent.date)
return nil
}
// 监听退出消息
func (u *user) HandleLogout(ctx context.Context, e event.Event) error {
logoutEvent, ok := e.EventValue().(*LogoutEvent)
if !ok {
return nil
}
fmt.Printf("you logout event at:%v\n", logoutEvent.date)
return nil
}
// 定义日志管理对象,需要监听用户登录、退出的消息
type logManager string
// 监听登录消息
func (u *logManager) HandleLogin(ctx context.Context, e event.Event) error {
loginEvent, ok := e.EventValue().(*LoginEvent)
if !ok {
return nil
}
fmt.Printf("user:%s handle login event at:%v\n", loginEvent.userID, loginEvent.date)
return nil
}
// 监听退出消息
func (u *logManager) HandleLogout(ctx context.Context, e event.Event) error {
logoutEvent, ok := e.EventValue().(*LogoutEvent)
if !ok {
return nil
}
fmt.Printf("user:%s handle logout event at:%v\n", logoutEvent.userID, logoutEvent.date)
return nil
}
结果
DEBUG msg=add consumer:824633794624
DEBUG msg=add consumer:824633794648
DEBUG msg=add consumer:824633794672
DEBUG msg=add consumer:824633794696
you login event at:2023-05-04 16:44:50.633516 +0800 CST m=+0.000290064
user:abc handle login event at:2023-05-04 16:44:50.633516 +0800 CST m=+0.000290064
you logout event at:2023-05-04 16:44:50.634023 +0800 CST m=+0.000797519
user:abc handle logout event at:2023-05-04 16:44:50.634023 +0800 CST m=+0.000797519
DEBUG msg=remove consumer:824633794712
you login event at:2023-05-04 16:44:50.633516 +0800 CST m=+0.000290064
user:abc handle login event at:2023-05-04 16:44:50.633516 +0800 CST m=+0.000290064
注:移除消费者有点问题,原因是consumer.ExecFunc(u.HandleLogin)
每次都是新生成一个对象,内存地址会变,ExecFunc.Name
函数需要改一下。
观察者模式优缺点
优点
1、降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
2、目标与观察者之间建立了一套触发机制。
缺点
1、目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
2、当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。
应用场景
观察者模式需要三个条件:观察者 被观察,话题订阅
实际场景可以是:
- 公众号推送消息,独立的微信号关注多个公众号 ,每次总能收到公众号发布的更新内容,订阅号也会将公众号置顶标红提醒
- 邮件订阅
- RSS Feeds
总结
今天花了一个多小时,学习了一下,并且实现了一个demo。完整功能后面再加,也欢迎感兴趣的同学一起完善,评论区留言即可。
参考
[1]观察者模式
[2]Go 设计模式-观察者模式
[3]源代码:https://github.com/ZBIGBEAR/observer