觀察者模式的實際應用

crossoverJie發表於2021-09-05

前言

設計模式不管是在面試還是工作中都會遇到,但我經常碰到小夥伴抱怨實際工作中自己應用設計模式的機會非常小。

正好最近工作中遇到一個用觀察者模式解決問題的場景,和大家一起分享。

背景如下:

在使用者建立完訂單的標準流程中需要做額外一些事情:

同時這些業務也是不固定的,隨時會根據業務發展增加、修改邏輯。

如果直接將邏輯寫在下單業務中,這一”坨“不是很核心的業務就會佔據的越來越多,修改時還有可能影響到正常的下單流程。

當然也有其他方案,比如可以啟動幾個定時任務,定期掃描掃描訂單然後實現自己的業務邏輯;但這樣會浪費許多不必要的請求。

觀察者模式

因此觀察者模式就應運而生,它是由事件釋出者在自身狀態發生變化時發出通知,由觀察者獲取訊息實現業務邏輯。

這樣事件釋出者和接收者就可以完全解耦,互不影響;本質上也是對開閉原則的一種實現。

示例程式碼

先大體看一下觀察者模式所使用到的介面與關係:

  • 主體介面:定義了註冊實現、迴圈通知介面。
  • 觀察者介面:定義了接收主體通知的介面。
  • 主體、觀察者介面都可以有多個實現。
  • 業務程式碼只需要使用 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 
  1. 建立一個建立訂單的主體 subject
  2. 註冊所有的訂閱事件。
  3. 在需要通知處呼叫 Notify 方法。

這樣一旦我們需要修改各個事件的實現時就不會互相影響,即便是要加入其他實現也是非常容易的:

  1. 編寫實現類。
  2. 註冊進實體。

不會再修改核心流程。

配合容器

其實我們也可以省略掉註冊事件的步驟,那就是使用容器;大致流程如下:

  1. 自定義的事件全部注入進容器。
  2. 再註冊事件的地方從容器中取出所有的事件,挨個註冊。

這裡所使用的容器是 https://github.com/uber-go/dig

修改後的程式碼中,每當我們新增一個觀察者(事件訂閱)時,只需要使用容器所提供 Provide 函式註冊進容器即可。

同時為了讓容器能夠支援同一個物件存在多個例項也需要新增部分程式碼:

Observer.go:

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

總結

有經驗的開發者會發現和釋出訂閱模式非常類似,當然他們的思路是類似的;我們不用糾結與兩者的差異(面試時除外);學會其中的思路更加重要。

相關文章