👀

Golang による Observer パターン

2022/01/30に公開

この記事について

GoF のデザインパターンの一つである Observer パターンについて解説してきます。実装は Go です。

概要

Observer パターンは観察対象のオブジェクトの状態が更新された場合に、観察者オブジェクトに対して通知を行うような処理を実装する際に使用されるパターンです。したがって状態変化に応じた処理を実装したい場合に有用なパターンとなります。

実装

実際に実装例を見ていきます。今回は何かしらメッセージが更新された場合に Slack や Email にメッセージを送信する処理を実装することを考えてみます。まずは基本的なインターフェースの実装を紹介します。

package observer

type Observer interface {
	Notify(generator MessageGenerator)
}

type MessageGenerator interface {
	AddObserver(observer Observer)
	DeleteObserver(observer Observer)
	GetMessage() string
	Execute()
}

Observer インターフェースは MessageGenaerator が更新したメッセージを通知する役割を担います。通知処理の実装はこのインターフェースを実装した構造体で行います。
MessageGenerator インターフェースは Observer の管理、メッセージの更新、各種 Observer への通知の役割を担います。

続けて具体的に上記のインターフェースを実装した構造体を実装していきます。以下のような実装になります。

package observer

type RandomMessageGenerator struct {
	observers []Observer
	message   string
}

func (r *RandomMessageGenerator) AddObserver(observer Observer) {
	r.observers = append(r.observers, observer)
}

func (r *RandomMessageGenerator) DeleteObserver(observer Observer) {
	var observers []Observer
	for _, e := range r.observers {
		if e != observer {
			observers = append(observers, e)
		}
	}
	r.observers = observers
}

func (r *RandomMessageGenerator) GetMessage() string {
	return r.message
}

func (r *RandomMessageGenerator) Execute() {
	r.message = fmt.Sprintf("message changed. number=%d", rand.Intn(100))
	for _, e := range r.observers {
		e.Notify(r)
	}
}

type SlackNotifier struct{}

func (s *SlackNotifier) Notify(generator MessageGenerator) {
	fmt.Printf("send message to slack. message = \"%s\"\n", generator.GetMessage())
}

type EmailNotifier struct{}

func (e *EmailNotifier) Notify(generator MessageGenerator) {
	fmt.Printf("send message to email. message = \"%s\"\n", generator.GetMessage())
}

RandomMessageGeneratorMessageGeneratorSlackNotifier および EmailNotifier はそれぞれ Observer インターフェースを実装しています(今回は通知処理に関してはこの記事の本質ではないので、それぞれ標準出力へ出力するのみにとどめています)。

上記の実装を踏まえて main() は以下のように実装します。

package main

func main() {
	generator := observer.RandomMessageGenerator{}
	slackNotifier := observer.SlackNotifier{}
	emailNotifier := observer.EmailNotifier{}
	generator.AddObserver(&slackNotifier)
	generator.AddObserver(&emailNotifier)
	generator.Execute()
}

実行すると以下のような内容が標準出力に出力されます。

send message to slack. message = "message changed. number=81"
send message to email. message = "message changed. number=81"

メリット

もう一度 RandomMessageGeneratorExecute() メソッドと SlackNotifierNotify() メソッドを見てみましょう。

func (r *RandomMessageGenerator) Execute() {
	r.message = fmt.Sprintf("message changed. number=%d", rand.Intn(100))
	for _, e := range r.observers {
		e.Notify(r)
	}
}

func (s *SlackNotifier) Notify(generator MessageGenerator) {
	fmt.Printf("send message to slack. message = \"%s\"\n", generator.GetMessage())
}

Execute() メソッドでは Observer インターフェースの Notify() メソッド、また Notify() メソッドでは MessageGenerator インターフェースの GetMessage() を呼んでいます。したがって RandomMessageGeneratorNotify() メソッドを呼び出す対象が SlackNotifier なのか EmailNotifier なのかは知らないですし、SlackNotifierGetMessage() メソッドを呼び出す対象が RandomMessageGenerator なのかは知りません。これが Observer パターンのメリットであり、 MessageGenerator および Observer インターフェースを実装した構造体は、それぞれインターフェースのメソッドを呼んで実装を行っているので、結合度が低く再利用可能なコード構成とすることが可能となります。

おまけ

今回は RandomMessageGeneratormessage フィールドの更新を各種 Observer に通知するような実装を行いました。上記実装ではメッセージの更新は main() で一回のみ行っていましたが、Observer 側でメッセージを更新するケースも場合によっては考えられると思います。具体的な例としては以下になります。

func (e *EmailNotifier) Notify(generator MessageGenerator) {
	fmt.Printf("send message to email. message = \"%s\"\n", generator.GetMessage())
	generator.Execute() // メッセージの更新
}

しかし、このような実装を行うと メッセージの更新 -> 通知 が無限に行われることになります。

send message to slack. message = "message changed. number=87"
send message to email. message = "message changed. number=87"
send message to slack. message = "message changed. number=12"
send message to email. message = "message changed. number=12"
send message to slack. message = "message changed. number=61"
send message to email. message = "message changed. number=61"
send message to slack. message = "message changed. number=35"
send message to email. message = "message changed. number=35"
...

これを防ぐためには RandomMessageGenerator 側で「現在通知を行っているかどうか」という情報をフラグとして持たせる必要があります。

type RandomMessageGenerator struct {
	observers []Observer
	message   string
	isNotifying bool
}

これを踏まえて Execute() メソッドを以下のように修正します。

func (r *RandomMessageGenerator) Execute() {
    r.message = fmt.Sprintf("message changed. number=%d", rand.Intn(100))
	if !r.isNotifying {
		r.isNotifying = true
		for _, e := range r.observers {
			e.Notify(r)
		}
	}
}

上記の実装とすることで通知は無限に行われずメッセージの更新のみ実行されるようになります。

Discussion