🐕

Goでオブザーバーパターンを実装する

2024/07/28に公開

オブザーバーパターンとは

イベントドリブンなアプリケーションで処理間を疎結合に連携させたいときに有効なデザインパターン。
いわゆるPub/Subで、イベントのプロデューサーは誰がSubscribeしているかを意識せずに、イベントをディスパッチャーに投げることで後続の処理を実行することができる。

詳細はWikipediaを参照

作るもの

以下を実装することによってオブザーバーパターンを実現する

  • イベント
  • イベントハンドラ
  • ディスパッチャー
    • Subscribeメソッド
      • イベントとイベントハンドラの組み合わせを登録
        • 1つのイベントで複数のハンドラを呼び出せる
    • Publishメソッド
      • 登録されたイベントハンドラにイベントを実行させる

最小限の構成

最終的なコード: github.com/regmarmcem/observer-pattern-in-go

実装

イベントは以下のインターフェースを定義する。

type Event interface {
	EventName() string
}

イベントは最低限イベント名を持っていてほしい。一方、イベントごとに異なるフィールドを持つことを許容したい。Goにはクラスがないので、イベント名をEventName()の実装で与えるようにする

イベントハンドラは以下のインターフェースを定義する。

type EventHandler interface {
	Handle(event Event)
}

Handleメソッドでは、Eventを受け取ってハンドラごとに好きな処理を実行する(実行結果などは管理しないので返り値はなし)。

ディスパッチャーは以下のようにイベントごとに複数のイベントハンドラを持つ構造体として実装する。

type Dispatcher struct {
	handlers map[string][]EventHandler
}

func (d *Dispatcher) Subscribe(event Event, handler EventHandler) {
	d.handlers[event.EventName()] = append(d.handlers[event.EventName()], handler)
}

func (d *Dispatcher) Publish(event Event) {
	handlers := d.handlers[event.EventName()]
	for _, handler := range handlers {
		handler.Handle(event)
	}
}

Subscribe()すると、Eventに対応するEventHandlerを持たせる
Publish()すると、対応するイベントハンドラをすべて呼び出して、すべて実行する

動作確認

以下のようにイベントを1つ、それに対応するハンドラを2つ用意してディスパッチャーにSubscribe, Publishする

type TestEvent struct {
	ID string
}

func (e TestEvent) EventName() string {
	return "TestEvent"
}

type HandlerOne struct{}

func (h HandlerOne) Handle(event Event) {
	fmt.Println("handler1")
	fmt.Println(event.EventName())
}

type HandlerTwo struct{}

func (h HandlerTwo) Handle(event Event) {
	fmt.Println("handler2")
	fmt.Println(event.EventName())
}

func main() {
	dispatcher := &Dispatcher{handlers: make(map[string][]EventHandler)}
	testEvent := TestEvent{ID: "xxxx"}
	handlerOne := HandlerOne{}
	handlerTwo := HandlerTwo{}

	dispatcher.Subscribe(testEvent, handlerOne)
	dispatcher.Subscribe(testEvent, handlerTwo)
	dispatcher.Publish(testEvent)
}

handler1, handler2の両方が呼び出される

➜  observer-pattern-in-go git:(main) go run .
handler1
TestEvent
handler2
TestEvent

実装を改善

該当箇所のコード: github.regmarmcem/observer-pattern-in-go

DispatcherへのSubscirbeは複数のgoroutineから呼ばれる可能性があるため、sync.Mutexによる排他制御を追加

package main
 
+import "sync"
+
type Dispatcher struct {
+       mu       sync.Mutex
        handlers map[string][]EventHandler
}
 
func (d *Dispatcher) Subscribe(event Event, handler EventHandler) {
+       d.mu.Lock()
+       defer d.mu.Unlock()
        d.handlers[event.EventName()] = append(d.handlers[event.EventName()], handler)
}

感想

  • モジュラモノリスなアプリケーションにはハマりそう
  • マイクロサービスの場合、オーケストレーター的なサービスにディスパッチャーの機能をもたせるみたいな構成になる(?)
    • ただし、イベントがオーケストレーターのメモリ上にしかないのはリスキーなので、永続化のために外部に書き出したりする処理が必要になりそう
    • その手間を払うくらいなら該当部分はクラウドのメッセージブローカーを使ったほうがいいかも

参考

https://qiita.com/yutorisan/items/6e960426da71b7e02af7
https://www.amazon.co.jp/Event-Driven-Architecture-Golang-asynchronicity-consistency/dp/1803238011

Discussion