Menu
Coddy logo textTech

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 メッセージングシステムで一般的に使用されます。

challenge icon

チャレンジ

簡単

Observerパターンを使って株価アラートシステムを構築しましょう!株価が変化するたびに複数のアラートサービスが自動的に通知を受け取るシステムを作成します。これは、subjectが具体的な型を知らなくても、observerがeventに反応できる仕組みを示すのに最適です。

コードは3つのファイルに分けて整理します。

  • observer.go: observerのinterfaceと具体的なobserver型をDefineします。

    Observer interfaceをCreateし、価格が変化したときに株式情報を受け取るUpdate(stockName string, price float64) methodを定義します。

    2種類のobserver型をImplementします。

    • PriceAlertThreshold field(float64)を持ち、更新されたとき、価格がしきい値を上回っていればALERT: [stockName] at $[price] crossed threshold $[threshold]を、下回っていればWATCHING: [stockName] at $[price] (threshold: $[threshold])を返します。
    • PriceLoggerLogName field(string)を持ち、更新されたとき、[logName] recorded [stockName]: $[price]を返します。

    両方のobserverに、整形済みの応答文字列を返すmethodを持たせます。

  • stock.go: observerを管理し、価格の変化を通知するsubjectをCreateします。

    Name(string)、Price(float64)、およびobserverのsliceをfieldsとして持つStock structを構築します。次のmethodをAddします。

    • Subscribe(o Observer):observerをAddします。
    • SetPrice(price float64) []string:価格を更新してall observerに通知し、すべての応答のsliceを返します。

    NewStock(name string, initialPrice float64) *Stock constructorも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行ずつ出力する
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Goオンラインコンパイラ