golang設計模式之觀察者模式

silsuer在掘金發表於2018-12-03

觀察者模式

寫在前面

定義

wiki: 觀察者模式是軟體設計模式的一種。在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。此種模式通常被用來實時事件處理系統。

簡單點說,可以想象成多個物件同時觀察一個物件,當這個被觀察的物件發生變化的時候,這些物件都會得到通知,可以做一些操作...

多囉嗦幾句

PHP世界中,最出名的觀察者模式應該就是 Laravel 的事件了,Laravel是一個事件驅動的框架,所有的操作都通過事件進行解耦,實現了一個簡單的觀察者模式,比較典型的一個使用就是資料庫模型,當觀察到模型更改的時候,就會觸發事件(created/updated/deleted...)

最開始用模型觀察者的時候,只要在 Observers 目錄中建立一個觀察者物件,並且新增觀察者關聯,當修改模型的時候,就可以自動觸發了,感覺好神奇喔...

觀察者模式在實際開發中經常用到,主要存在於底層框架中,與業務邏輯解耦,業務邏輯只需要實現各種觀察者被觀察者即可。

類圖

golang設計模式之觀察者模式

(圖源網路)

角色

  • 抽象觀察者

  • 具體觀察者

  • 抽象被觀察者

  • 具體被觀察者

舉個栗子

  1. 建立抽象觀察者
  // 抽象觀察者
  type IObserver interface {
  	Notify() // 當被觀察物件有更改的時候,出發觀察者的Notify() 方法
  }
複製程式碼
  1. 建立抽象被觀察者
// 抽象被觀察者
type ISubject interface {
    AddObservers(observers ...IObserver) // 新增觀察者
    NotifyObservers()                    // 通知觀察者
}
複製程式碼
  1. 實現觀察者
 
 type Observer struct {
 }

 func (o *Observer) Notify() {
     fmt.Println("已經觸發了觀察者")
 }
複製程式碼
  1. 實現被觀察者
 
 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() // 觸發觀察者
     }
 }
複製程式碼
  1. 使用例項
     // 建立被觀察者
      s := new(Subject)
      // 建立觀察者
      o := new(Observer)
      // 為主題新增觀察者
      s.AddObservers(o)
  
      // 這裡的被觀察者要做各種更改...
  
      // 更改完畢,觸發觀察者
      s.NotifyObservers()  // output: 已經觸發了觀察者
複製程式碼

舉個實際應用的例子

PHP 熟悉的同學可以看看這個,Laravel中的事件系統觀察者模式

下面寫一個我自己專案中的栗子: github.com/silsuer/bin…

這是一個golang語言實現的事件系統,我正在試著把它應用到自己的框架中,它實現了兩種觀察者模式,一種是實現了觀察者介面的觀察者模式,一種是使用了反射進行型別對映的觀察者模式,下面分別來說一下...

  1. 實現了觀察者介面的方式:
   // 建立一個結構體,實現事件介面
   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的路由包 中寫過,用來做中介軟體攔截的操作。

  1. 使用反射做觀察者對映的方式

      // 建立一個物件,不必實現事件介面
      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 ~

相關文章