觀察者模式
寫在前面
定義
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() // output: 已經觸發了觀察者
複製程式碼
舉個實際應用的例子
對PHP
熟悉的同學可以看看這個,Laravel中的事件系統和觀察者模式
下面寫一個我自己專案中的栗子: github.com/silsuer/bin…
這是一個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 ~