100行代码实现一个观察者模式,开箱即用


观察者模式介绍

观察者模式(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


文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录