观察者模式
写在前面
定义
wiki: 观察者模式是软体设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。
简单点说,可以想象成多个对象同时观察一个对象,当这个被观察的对象发生变化的时候,这些对象都会得到通知,可以做一些操作...
多啰嗦几句
在PHP
世界中,最出名的观察者模式应该就是 Laravel
的事件了,Laravel
是一个事件驱动的框架,所有的操作都通过事件进行解耦,实现了一个简单的观察者模式,比较典型的一个使用就是数据库模型,当观察到模型更改的时候,就会触发事件(created
/updated
/deleted
...)
最开始用模型观察者的时候,只要在 Observers
目录中创建一个观察者对象,并且添加观察者关联,就可以自动触发了,好神奇喔...
观察者模式在实际开发中经常用到,主要存在于底层框架中,与业务逻辑解耦,业务逻辑只需要实现各种观察者被观察者即可。
类图
(图源网络)
角色
-
抽象观察者
-
具体观察者
-
抽象被观察者
-
具体被观察者
举个栗子
- 创建抽象观察者
// 抽象观察者
type IObserver interface {
Notify() // 当被观察对象有更改的时候,出发观察者的Notify() 方法
}
- 创建抽象被观察者
// 抽象被观察者
type ISubject interface {
AddObservers(observers ...IObserver) // 添加观察者
NotifyObservers() // 通知观察者
}
- 实现观察者
type Observer struct {
}
func (o *Observer) Notify() {
fmt.Println("已经触发了观察者")
}
- 实现被观察者
type Subject struct {
observers []IObserver
}
func (s *Subject) AddObservers(observers ...IObserver) {
s.observers = append(s.observers, observers...)
}
func (s *Subject) NotifyObservers() {
for k := range s.observers {
s.observers[k].Notify() // 触发观察者
}
}
- 使用实例
// 创建被观察者
s := new(Subject)
// 创建观察者
o := new(Observer)
// 为主题添加观察者
s.AddObservers(o)
// 这里的被观察者要做各种更改...
// 更改完毕,触发观察者
s.NotifyObservers()
举个实际应用的例子
对PHP
熟悉的同学可以看看这个,Laravel中的事件系统和观察者模式
下面写一个我自己项目中的栗子: github.com/silsuer/bingo-events
这是一个golang
语言实现的事件系统,我正在试着把它应用到自己的框架中,它实现了两种观察者模式,一种是实现了观察者接口的观察者模式,一种是使用了反射进行类型映射的观察者模式,下面分别来说一下...
- 实现了观察者接口的方式:
// 创建一个结构体,实现事件接口
type listen struct {
bingo_events.Event
Name string
}
func func main() {
// 事件对象
app := bingo_events.NewApp()
l := new(listen) // 创建被观察者
l.Name = "silsuer"
l.Attach(func (event interface{}, next func(event interface{})) {
// 由于监听器可监听的对象不一定非要实现 IEvent 接口,所以这里需要使用类型断言,将对象转换回原本的类型
a := event.(*listen)
fmt.Println(a.Name) // output: silsuer
a.Name = "god" // 更改结构体的属性
next(a) // 调用next函数继续走下一个监听器,如果此处不调用,程序将会终止在此处,不会继续往下执行
})
// 触发事件
app.Dispatch(l)
}
这里我们使用结构体组合的形式实现了事件接口的实现,只要把bingo-events.Event
放入要监听的结构体中,就实现了IEvent
接口,可以使用 Attach()
方法来添加观察者了,
这里的观察者是一个 func (event interface{}, next func(event interface{}))
类型的方法,
第一个参数是触发的对象,毕竟观察者有时需要用到被观察者的属性,比如上面提到的Laravel
的模型...
第二个参数是一个 func(event interface{})
类型的方法,实际上这里实现了一个 pipeline
,来做拦截功能,可以实现观察者的截断,只有在观察者的最后调用了 next()
方法,事件才会继续向下一个观察者传递,
pipeline
的原理我在 参考Laravel制作基于golang的路由包 中写过,用来做中间件拦截的操作。
-
使用反射做观察者映射的方式
// 创建一个对象,不必实现事件接口 type listen struct { Name string } func main() { // 事件对象 app := bingo_events.NewApp() // 添加观察者 app.Listen("*main.listen", ListenStruct) // 直接使用 Listen 方法,为监听的结构体添加一个回调 l := new(listen) l.Name = "silsuer" // 为对象属性赋值 // 复制完毕,开始分发事件,从上可知,共添加了两个观察者: ListenStruct 和 L2 // 会按照监听的顺序执行,由于最开始已经添加了 ListenStruct 监听器,所以第二次再次添加的时候不会重复添加 // 此处的分发,就是将参数顺序传入每一个监听器,进行后续操作 app.Dispatch(l) } func ListenStruct(event interface{}, next func(event interface{})) { // 由于监听器可监听的对象不一定非要实现 IEvent 接口,所以这里需要使用类型断言,将对象转换回原本的类型 a := event.(*listen) fmt.Println(a.Name) // output: silsuer a.Name = "god" // 更改结构体的属性 next(a) // 调用next函数继续走下一个监听器,如果此处不调用,程序将会终止在此处,不会继续往下执行 }
这里我们没有使用实现了事件对象的
Attach
方法来添加观察者,而是使用一个字符串代表被观察者,这样就做到了无需实现观察者接口,就可以做到观察者模式。完整代码可以直接去看git仓库这里我们需要关注两个方法:
Listen()
和Dispatch()
bingo_events.App
是一个服务容器,装载了所有事件和事件之间的映射关系// 服务容器 type App struct { sync.RWMutex // 读写锁 events map[string][]Listener // 事件映射 }
下面看源代码:
Listen()
:// 监听[事件][监听器],单独绑定一个监听器 func (app *App) Listen(str string, listener Listener) { app.Lock() app.events[str] = append(app.events[str], listener) app.Unlock() }
app.events
中保存的是通过字符串监听的对象,字符串就是通过reflect.TypeOf(v).String()
得到的字符串,例如上面的listen
对象就是*main.listen
,这是键,对应的值就是绑定的监听器方法Dispatch()
// 分发事件,传入各种事件,如果是 func (app *App) Dispatch(events ...interface{}) { // 容器分发数据 var event string for k := range events { if _, ok := events[k].(string); ok { // 如果传入的是字符串类型的 event = events[k].(string) } else { // 不是字符串类型的,那么得到其类型 event = reflect.TypeOf(events[k]).String() } // 如果实现了 事件接口 IEvent,则调用事件的观察者模式,得到所有的 var observers []Listener if _, ok := events[k].(IEvent); ok { obs := events[k].(IEvent).Observers() observers = append(observers, obs...) // 将事件中自行添加的观察者,放在所有观察者之后 } // 如果存在map映射,则也放入观察者数组中 if obs, exist := app.events[event]; exist { observers = append(observers, obs...) } if len(observers) > 0 { // 得到了所有的观察者,这里通过pipeline来执行,通过next来控制什么时候调用这个观察者 new(Pipeline).Send(events[k]).Through(observers).Then(func(context interface{}) { }) } } }
Dispatch()
方法传入一个对象(或对象类型的字符串),是否实现事件接口都可以,遍历所有对象(如果传入的是字符串,就不做处理,如果是对象,通过反射提取对象的字符串名称),然后从当前App
中的map
提取对应的观察者,如果对象还实现了事件接口,则再获取挂载在这个事件上的所有观察者,将他们装在同一个slice
中,构建成一个pipeline
,顺序执行观察者。
上述代码均放在 golang-design-patterns 这个仓库中
打个广告,推荐一下自己写的 go web框架 bingo,求star,求PR ~