觀察者模式的實際應用
本文轉載自微信公眾號「crossoverJie」,作者crossoverJie。轉載本文請聯系crossoverJie公眾號。
前言
設計模式不管是在面試還是工作中都會遇到,但我經常碰到小伙伴抱怨實際工作中自己應用設計模式的機會非常小。
正好最近工作中遇到一個用觀察者模式解決問題的場景,和大家一起分享。
背景如下:
在用戶創建完訂單的標準流程中需要做額外一些事情:
同時這些業務也是不固定的,隨時會根據業務發展增加、修改邏輯。
如果直接將邏輯寫在下單業務中,這一”坨“不是很核心的業務就會占據的越來越多,修改時還有可能影響到正常的下單流程。
當然也有其他方案,比如可以啟動幾個定時任務,定期掃描掃描訂單然后實現自己的業務邏輯;但這樣會浪費許多不必要的請求。
觀察者模式
因此觀察者模式就應運而生,它是由事件發布者在自身狀態發生變化時發出通知,由觀察者獲取消息實現業務邏輯。
這樣事件發布者和接收者就可以完全解耦,互不影響;本質上也是對開閉原則的一種實現。
示例代碼
先大體看一下觀察者模式所使用到的接口與關系:
- 主體接口:定義了注冊實現、循環通知接口。
- 觀察者接口:定義了接收主體通知的接口。
- 主體、觀察者接口都可以有多個實現。
- 業務代碼只需要使用 Subject.Nofity() 接口即可。
接下來看看創建訂單過程中的實現案例。
代碼采用 go 實現,其他語言也是類似。
首先按照上圖定義了兩個接口:
- type Subject interface {
- Register(Observer)
- Notify(data interface{})
- }
- type Observer interface {
- Update(data interface{})
- }
由于我們這是一個下單的事件,所以定義了 OrderCreateSubject 實現 Subject:
- type OrderCreateSubject struct {
- observerList []Observer
- }
- func NewOrderCreate() Subject {
- return &OrderCreateSubject{}
- }
- func (o *OrderCreateSubject) Register(observer Observer) {
- o.observerList = append(o.observerList, observer)
- }
- func (o *OrderCreateSubject) Notify(data interface{}) {
- for _, observer := range o.observerList {
- observer.Update(data)
- }
- }
其中的 observerList 切片是用于存放所有訂閱了下單事件的觀察者。
接著便是編寫觀察者業務邏輯了,這里我實現了兩個:
- type B1CreateOrder struct {
- }
- func (b *B1CreateOrder) Update(data interface{}) {
- fmt.Printf("b1.....data %v \n", data)
- }
- type B2CreateOrder struct {
- }
- func (b *B2CreateOrder) Update(data interface{}) {
- fmt.Printf("b2.....data %v \n", data)
- }
使用起來也非常簡單:
- func TestObserver(t *testing.T) {
- create := NewOrderCreate()
- create.Register(&B1CreateOrder{})
- create.Register(&B2CreateOrder{})
- create.Notify("abc123")
- }
Output:
- b1.....data abc123
- b2.....data abc123
- 創建一個創建訂單的主體 subject。
- 注冊所有的訂閱事件。
- 在需要通知處調用 Notify 方法。
這樣一旦我們需要修改各個事件的實現時就不會互相影響,即便是要加入其他實現也是非常容易的:
- 編寫實現類。
- 注冊進實體。
不會再修改核心流程。
配合容器
其實我們也可以省略掉注冊事件的步驟,那就是使用容器;大致流程如下:
自定義的事件全部注入進容器。
再注冊事件的地方從容器中取出所有的事件,挨個注冊。
這里所使用的容器是 https://github.com/uber-go/dig
修改后的代碼中,每當我們新增一個觀察者(事件訂閱)時,只需要使用容器所提供 Provide 函數注冊進容器即可。
同時為了讓容器能夠支持同一個對象存在多個實例也需要新增部分代碼:
- type Observer interface {
- Update(data interface{})
- }
- type (
- Instance struct {
- dig.Out
- Instance Observer `group:"observers"`
- }
- InstanceParams struct {
- dig.In
- Instances []Observer `group:"observers"`
- }
- )
在 observer 接口中需要新增兩個結構體用于存放同一個接口的多個實例。
group:"observers" 用于聲明是同一個接口。
創建具體觀察者對象時返回 Instance 對象。
- func NewB1() Instance {
- return Instance{
- Instance: &B1CreateOrder{},
- }
- }
- func NewB2() Instance {
- return Instance{
- Instance: &B2CreateOrder{},
- }
- }
其實就是用 Instance 包裝了一次。
這樣在注冊觀察者時,便能從 InstanceParams.Instances 中取出所有的觀察者對象了。
- err = c.Invoke(func(subject Subject, params InstanceParams) {
- for _, instance := range params.Instances {
- subject.Register(instance)
- }
- })
這樣在使用時直接從容器中獲取主題對象,然后通知即可:
- err = c.Invoke(func(subject Subject) {
- subject.Notify("abc123")
- })
更多關于 dig 的用法可以參考官方文檔:
https://pkg.go.dev/go.uber.org/dig#hdr-Value_Groups
總結
有經驗的開發者會發現和發布訂閱模式非常類似,當然他們的思路是類似的;我們不用糾結與兩者的差異(面試時除外);學會其中的思路更加重要。