Observer パターン
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 89/107。
Observerパターンは、オブジェクト間の一対多の関係を定義します。1つのオブジェクト(サブジェクト)の状態が変化すると、そのすべての依存オブジェクト(observer)に自動的に通知されます。これは、eventシステム、通知、または複数のコンポーネントが変更に対応する必要があるあらゆる状況に適しています。
Goでは、オブザーバーにはインターフェースを使用し、サブスクリプションを管理するsubject構造体を使ってこのパターンを実装します。
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}Observerインターフェースを実装する任意の型は、通知を受け取るために購読できます。
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}subject は具体的な型を知らずに、すべての observer に通知します。
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// user@example.com へのメール: Order placed
// ログ: Order placedこのパターンは、subject とその observer を分離するため、既存のコードを変更せずに新しい observer の型を簡単に追加できます。これは、イベント駆動型アーキテクチャ、GUI フレームワーク、pub/sub メッセージングシステムで一般的に使用されます。
チャレンジ
簡単Observerパターンを使って株価アラートシステムを構築しましょう!株価が変化するたびに複数のアラートサービスが自動的に通知を受け取るシステムを作成します。これは、subjectが具体的な型を知らなくても、observerがeventに反応できる仕組みを示すのに最適です。
コードは3つのファイルに分けて整理します。
observer.go: observerのinterfaceと具体的なobserver型をDefineします。ObserverinterfaceをCreateし、価格が変化したときに株式情報を受け取るUpdate(stockName string, price float64)methodを定義します。2種類のobserver型をImplementします。
PriceAlert:Thresholdfield(float64)を持ち、更新されたとき、価格がしきい値を上回っていればALERT: [stockName] at $[price] crossed threshold $[threshold]を、下回っていればWATCHING: [stockName] at $[price] (threshold: $[threshold])を返します。PriceLogger:LogNamefield(string)を持ち、更新されたとき、[logName] recorded [stockName]: $[price]を返します。
両方のobserverに、整形済みの応答文字列を返すmethodを持たせます。
stock.go: observerを管理し、価格の変化を通知するsubjectをCreateします。Name(string)、Price(float64)、およびobserverのsliceをfieldsとして持つStockstructを構築します。次のmethodをAddします。Subscribe(o Observer):observerをAddします。SetPrice(price float64) []string:価格を更新してall observerに通知し、すべての応答のsliceを返します。
NewStock(name string, initialPrice float64) *StockconstructorもCreateします。main.go: observerを持つstockを設定し、価格の変化をシミュレートします。stock nameとinitial priceを読み取ります。次に、Createするobserverの数を読み取ります。各observerについて、そのtype(
alertまたはlogger)とconfiguration(alertの場合はthreshold、loggerの場合はlog name)を読み取ります。各observerをstockにSubscribeします。最後にnew priceを読み取り、stockを更新して、各observerの応答をそれぞれ1行に出力します。
次の入力が提供されます。
- 1行目:Stock name
- 2行目:Initial price
- 3行目:Number of observers
- 各observer:type(
alertまたはlogger)、続いてconfiguration value - 最終行:設定するNew price
すべての価格は小数点以下2桁でFormatします。
たとえば、次の入力がgivenされた場合:
GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50出力は次のようになります。
ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00また、次の入力がgivenされた場合:
AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25出力は次のようになります。
DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)Stockがalertに通知しているのかloggerに通知しているのかを知らない点に注目してください。Stockは各observerでUpdateを呼び出すだけです。この疎結合により、Stockのコードを変更せずに新しいobserver typeを簡単にAddできます!
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 銘柄名を読み取る
stockName, _ := reader.ReadString('\n')
stockName = strings.TrimSpace(stockName)
// 初期価格を読み取る
initialPriceStr, _ := reader.ReadString('\n')
initialPrice, _ := strconv.ParseFloat(strings.TrimSpace(initialPriceStr), 64)
// オブザーバーの数を読み取る
numObserversStr, _ := reader.ReadString('\n')
numObservers, _ := strconv.Atoi(strings.TrimSpace(numObserversStr))
// TODO: NewStockコンストラクタを使用して新しいStockを作成する
// TODO: 各オブザーバーをループ処理する
for i := 0; i < numObservers; i++ {
// オブザーバータイプ("alert"または"logger")を読み取る
observerType, _ := reader.ReadString('\n')
observerType = strings.TrimSpace(observerType)
// 設定値を読み取る
configValue, _ := reader.ReadString('\n')
configValue = strings.TrimSpace(configValue)
// TODO: タイプに基づいて適切なオブザーバーを作成する
// - "alert"の場合: configValueをfloat64の閾値として解析し、PriceAlertを作成する
// - "logger"の場合: configValueをLogNameとして使用し、PriceLoggerを作成する
// TODO: オブザーバーをstockに登録する
}
// 新しい価格を読み取る
newPriceStr, _ := reader.ReadString('\n')
newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)
// TODO: 新しい価格を設定してレスポンスを取得する
// TODO: 各レスポンスを1行ずつ出力する
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー自分で練習してみよう: Goオンラインコンパイラ