Menu
Coddy logo textTech

Observer パターン

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 89/107。

オブザーバーパターンは、オブジェクト間の1対多の依存関係を定義します。1つのオブジェクト(サブジェクト)の状態が変化すると、そのすべての依存オブジェクト(オブザーバー)に自動的に通知されます。これは、イベントシステム、通知、または複数のコンポーネントが変更に反応する必要があるあらゆるシナリオに最適です。

Goでは、オブザーバー用のインターフェースと、サブスクリプションを管理するサブジェクト構造体を使用して、このパターンを実装します:

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は、具体的な型を知ることなく、すべてのオブザーバーに通知します:

subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})

subject.Notify("Order placed")
// user@example.com へのメール: Order placed
// ログ: Order placed

このパターンは、サブジェクトとそのオブザーバーを分離し、既存のコードを変更することなく新しいオブザーバーの種類を簡単に追加できるようにします。これは、イベント駆動型アーキテクチャ、GUIフレームワーク、およびパブリッシュ/サブスクライブ(pub/sub)メッセージングシステムで一般的に使用されています。

challenge icon

チャレンジ

簡単

Observer パターンを使用して、株価アラートシステムを構築しましょう!株価が変動するたびに複数のアラートサービスが自動的に通知を受け取るシステムを作成します。これは、サブジェクト(監視対象)が具体的な型を知らなくても、オブザーバーがイベントに反応できることを示すのに最適です。

コードは 3 つのファイルに分けて構成します:

  • observer.go: オブザーバーのインターフェースと具体的なオブザーバーの型を定義します。

    価格が変動したときに株の情報を受け取る Update(stockName string, price float64) メソッドを持つ Observer インターフェースを作成します。

    2 つのオブザーバー型を実装します:

    • PriceAlert: Threshold フィールド (float64) を持ちます。更新時、価格がしきい値を超えている場合は ALERT: [stockName] at $[price] crossed threshold $[threshold] を返し、しきい値以下の場合は WATCHING: [stockName] at $[price] (threshold: $[threshold]) を返します。
    • PriceLogger: LogName フィールド (string) を持ちます。更新時、[logName] recorded [stockName]: $[price] を返します。

    両方のオブザーバーは、フォーマットされたレスポンス文字列を返すメソッドを持つ必要があります。

  • stock.go: オブザーバーを管理し、価格変動を通知するサブジェクトを作成します。

    Name (string)、Price (float64)、およびオブザーバーのスライスをフィールドに持つ Stock 構造体を構築します。以下のメソッドを追加してください:

    • Subscribe(o Observer): オブザーバーを追加します。
    • SetPrice(price float64) []string: 価格を更新し、すべてのオブザーバーに通知して、それらすべてのレスポンスのスライスを返します。

    また、NewStock(name string, initialPrice float64) *Stock コンストラクタも作成してください。

  • main.go: オブザーバーを持つ株をセットアップし、価格変動をシミュレートします。

    株の名前と初期価格を読み取ります。次に、作成するオブザーバーの数を読み取ります。各オブザーバーについて、そのタイプ(alert または logger)と設定値(アラートの場合はしきい値、ロガーの場合はログ名)を読み取ります。各オブザーバーを株に購読(Subscribe)させます。

    最後に、新しい価格を読み取り、株を更新し、各オブザーバーのレスポンスをそれぞれの行に出力します。

以下の入力が提供されます:

  • 1行目: 株の名前
  • 2行目: 初期価格
  • 3行目: オブザーバーの数
  • 各オブザーバーについて: タイプ(alert または logger)、その後に設定値
  • 最終行: 設定する新しい価格

すべての価格は小数点以下2桁でフォーマットしてください。

例えば、以下のように入力された場合:

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

また、以下のように入力された場合:

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 がアラートを通知しているのかロガーを通知しているのかを関知せず、単に各オブザーバーの Update を呼び出している点に注目してください。この疎結合(decoupling)により、Stock のコードを変更することなく、新しいオブザーバー型を簡単に追加できるようになります!

チートシート

Observer パターンは、サブジェクトの状態が変化したときに、複数のオブザーバーに自動的に通知する 1 対多のリレーションシップを定義します。これにより、サブジェクトとオブザーバーが疎結合になり、イベントシステムや通知に最適です。

基本構造

オブザーバーインターフェースと、購読を管理するサブジェクトを定義します:

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 := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})

subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placed

サブジェクトは具体的な型を知ることなくすべてのオブザーバーに通知するため、既存のコードを変更せずに新しいオブザーバー型を簡単に追加できます。

自分で試してみよう

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: 各レスポンスをそれぞれの行に出力する
}
quiz icon腕試し

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

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