Goでデザインパターン (Observerパターン)

Goデザインパターン

Observerパターンとは

振る舞いに関するデザインパターンの一つ。サブスクリプションの仕組みを用意し、特定のオブジェクトの状態変化等のイベントが発生した時に、購読登録を行っている複数のオブジェクトにその通知を行うことができる。MVCアーキテクチャではObserverパターンが利用されている。ViewがSubscriber、ModelがPublisherになっており、ビジネスロジック・内部データ(Model)と表示レイアウト(View)を分離するために利用されている。

長所

  • 新規のPublisherやSubscriberを追加する際に、既存のPublisher, Subscriberに変更が必要ない。(開放閉鎖の原則)

短所

  • 通知が行われる順番は不定である。
  • Subscriberの状態変化を切っ掛けにPublisherの通知が行われるような実装の時、その通知を契機にSubscriberの状態変化が起きる可能性がある場合は無限ループにならないように気を付ける必要がある。

利用場面

  • あるオブジェクトの状態変化等のイベントが発生した時に、その情報を全く別のオブジェクトに通知したい場合。
  • あるオブジェクトの状態監視を期間限定または特定の条件下でのみ行いたい場合。

クラス図

Observerパターン
  • 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]

コメント

タイトルとURLをコピーしました