Observerパターンとは
振る舞いに関するデザインパターンの一つ。サブスクリプションの仕組みを用意し、特定のオブジェクトの状態変化等のイベントが発生した時に、購読登録を行っている複数のオブジェクトにその通知を行うことができる。MVCアーキテクチャではObserverパターンが利用されている。ViewがSubscriber、ModelがPublisherになっており、ビジネスロジック・内部データ(Model)と表示レイアウト(View)を分離するために利用されている。
長所
- 新規のPublisherやSubscriberを追加する際に、既存のPublisher, Subscriberに変更が必要ない。(開放閉鎖の原則)
短所
- 通知が行われる順番は不定である。
- Subscriberの状態変化を切っ掛けにPublisherの通知が行われるような実装の時、その通知を契機にSubscriberの状態変化が起きる可能性がある場合は無限ループにならないように気を付ける必要がある。
利用場面
- あるオブジェクトの状態変化等のイベントが発生した時に、その情報を全く別のオブジェクトに通知したい場合。
- あるオブジェクトの状態監視を期間限定または特定の条件下でのみ行いたい場合。
クラス図

- Publisher
購読開始/停止メソッドとイベント通知用メソッドを宣言する。 - Concrete Publisher
Publisherインターフェースを実装する。状態が変化した時や何らかの処理を行った時に、そのことをSubscriber達にイベント通知用メソッドを利用して通知する。 - Subscriber
Publisherからイベント通知を受け取るメソッドを宣言する。Publisherからイベントの詳細情報を引数として受け取ることができる。 - Concrete Subscriber
Subscriberインターフェースを実装し、Publisherからの通知に応じて何らかの処理を行う。
実装例
Publisher
package main
type Publisher interface {
Subscribe(s Subscriber)
Unsubscribe(s Subscriber)
NotifiyAll()
}
Concrete Publisher
package main
import (
"math/rand"
"time"
"fmt"
)
type EarthquakeSensor struct {
subscribers []Subscriber
}
func NewEarthquakeSensor() *EarthquakeSensor {
return &EarthquakeSensor{}
}
// Subscriberを引数として受け取り、購読登録処理を行う。
// 関数を引数として渡せる場合は、構造体ではなく関数を引数として実装することも可能。
func (s *EarthquakeSensor) Subscribe(sub Subscriber) {
s.subscribers = append(s.subscribers, sub)
}
func (s *EarthquakeSensor) Unsubscribe(sub Subscriber) {
for i, so := range s.subscribers {
if so == sub {
s.subscribers = append(s.subscribers[:i], s.subscribers[i+1:]...)
}
}
}
func (s *EarthquakeSensor) NotifiyAll() {
rand.Seed(time.Now().UnixNano())
t := time.Date(2022, time.Month(rand.Intn(12)), rand.Intn(29), rand.Intn(24), rand.Intn(60), rand.Intn(60), 0, time.UTC)
notification := NewNotification(
"Earthquake Alert",
fmt.Sprintf("The earthquake occurred at %s", t.Format("2006-01-02 15:4:5")),
)
for _, o := range s.subscribers {
o.Update(*notification)
}
}
Subscriber
package main
type Subscriber interface {
Update(n Notification) // Publisherからの更新通知に応答する処理
}
Concrete Subscriber
package main
import (
"fmt"
)
type PCNotifier struct {
name string
}
func NewPCNotifier() *PCNotifier {
return &PCNotifier{name: "PC"}
}
func (p PCNotifier) Update(n Notification) {
fmt.Printf("[%s] %s (to %s)\n", n.Title, n.Message, p.name)
}
package main
import (
"fmt"
)
type MobileNotifier struct {
name string
}
func NewMobileNotifier() *MobileNotifier {
return &MobileNotifier{name: "Mobile"}
}
func (m *MobileNotifier) Update(n Notification) {
fmt.Printf("[%s] %s (to %s)\n", n.Title, n.Message, m.name)
}
その他
package main
type Notification struct {
Title string
Message string
}
func NewNotification(title, message string) *Notification {
return &Notification{
Title: title,
Message: message,
}
}
動作確認
package main
func main() {
pn := NewPCNotifier()
mn := NewMobileNotifier()
sensor := NewEarthquakeSensor()
sensor.Subscribe(pn)
sensor.Subscribe(mn)
sensor.NotifiyAll()
sensor.Unsubscribe(mn)
sensor.NotifiyAll()
}
>> go run .
[Earthquake Alert] The earthquake occurred at 2022-09-17 00:26:56 (to PC)
[Earthquake Alert] The earthquake occurred at 2022-09-17 00:26:56 (to Mobile)
[Earthquake Alert] The earthquake occurred at 2022-04-12 23:0:32 (to PC)
補足
Goは関数型をサポートしているため、Subscriber構造体を用意せず、関数そのものを購読登録処理に引数として渡すように実装することもできる。
package main
import (
"math/rand"
"time"
"fmt"
)
type EarthquakeSensorForFunction struct {
functions []func(n Notification)
}
func NewEarthquakeSensorForFunction() *EarthquakeSensorForFunction {
return &EarthquakeSensorForFunction{}
}
func (s *EarthquakeSensorForFunction) Subscribe(f func(n Notification)) (id int) {
id = len(s.functions)
s.functions = append(s.functions, f)
return id
}
func (s *EarthquakeSensorForFunction) Unsubscribe(id int) {
if id < len(s.functions) {
s.functions = append(s.functions[:id], s.functions[id+1:]...)
}
}
func (s *EarthquakeSensorForFunction) NotifiyAll() {
rand.Seed(time.Now().UnixNano())
t := time.Date(2022, time.Month(rand.Intn(12)), rand.Intn(29), rand.Intn(24), rand.Intn(60), rand.Intn(60), 0, time.UTC)
notification := NewNotification(
"Earthquake Alert",
fmt.Sprintf("The earthquake occurred at %s", t.Format("2006-01-02 15:4:5")),
)
for _, f := range s.functions {
f(*notification)
}
}
package main
import (
"fmt"
)
func main() {
sensorForFunc := NewEarthquakeSensorForFunction()
f_pc := func(n Notification) {
fmt.Printf("[%s] %s (to PC) [Use Function]\n", n.Title, n.Message)
}
f_mobile := func(n Notification) {
fmt.Printf("[%s] %s (to Mobile) [Use Function]\n", n.Title, n.Message)
}
pc_id := sensorForFunc.Subscribe(f_pc)
sensorForFunc.Subscribe(f_mobile)
sensorForFunc.NotifiyAll()
sensorForFunc.Unsubscribe(pc_id)
sensorForFunc.NotifiyAll()
}
>> go run .
[Earthquake Alert] The earthquake occurred at 2022-08-05 01:47:34 (to PC) [Use Function]
[Earthquake Alert] The earthquake occurred at 2022-08-05 01:47:34 (to Mobile) [Use Function]
[Earthquake Alert] The earthquake occurred at 2022-03-01 15:39:55 (to Mobile) [Use Function]
コメント