Menu
Coddy logo textTech

Паттерн Observer

Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 89 из 107.

Паттерн Observer pattern определяет отношение «один ко многим» между объектами. Когда один объект (субъект) изменяет состояние, все его зависимые объекты (наблюдатели) автоматически получают уведомление. Это идеально подходит для систем событий, уведомлений или любых сценариев, в которых нескольким компонентам необходимо реагировать на изменения.

В Go мы реализуем этот паттерн, используя interface для observer и структуру субъекта, которая управляет подписками:

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)
}

Субъект notifies всех наблюдателей, не зная их конкретных типов:

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

subject.Notify("Order placed")
// Email на user@example.com: Order placed
// Лог: Order placed

Этот паттерн отделяет subject от его наблюдателей, благодаря чему легко добавлять новые типы наблюдателей, не изменяя существующий код. Он часто используется в архитектурах, управляемых событиями, GUI-фреймворках и системах обмена сообщениями pub/sub.

challenge icon

Задание

Легко

Давайте создадим систему оповещений об изменении цены акций с использованием паттерна Observer! Вы создадите систему, в которой несколько служб оповещений автоматически получают уведомления всякий раз, когда цена акции изменяется. Это отлично демонстрирует, как наблюдатели могут реагировать на события, не зная конкретных типов субъекта.

Вы распределите код по трём файлам:

  • observer.go: Define интерфейс наблюдателя и конкретные типы наблюдателей.

    Создайте интерфейс Observer с методом Update(stockName string, price float64), который получает информацию об акции при изменении цены.

    Implement два типа наблюдателей:

    • 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: Create субъект, который управляет наблюдателями и уведомляет их об изменениях цены.

    Создайте структуру Stock с полями Name (string), Price (float64) и срезом наблюдателей. Add следующие методы:

    • Subscribe(o Observer) для добавления наблюдателя
    • SetPrice(price float64) []string, который обновляет цену и уведомляет всех наблюдателей, возвращая срез всех их ответов

    Также создайте конструктор NewStock(name string, initialPrice float64) *Stock.

  • main.go: Настройте акцию с наблюдателями и смоделируйте изменения цены.

    Считайте название акции и начальную цену. Затем считайте количество наблюдателей, которые нужно создать. Для каждого наблюдателя считайте его тип (alert или logger) и его конфигурацию (пороговое значение для оповещений, имя журнала для журналов). Подпишите каждого наблюдателя на акцию.

    Наконец, считайте новую цену, обновите акцию и выведите ответ каждого наблюдателя в отдельной строке.

Будут предоставлены следующие входные данные:

  • Строка 1: название акции
  • Строка 2: начальная цена
  • Строка 3: количество наблюдателей
  • Для каждого наблюдателя: тип (alert или logger), затем значение конфигурации
  • Последняя строка: новая цена, которую нужно установить

Форматируйте все цены с двумя знаками после запятой.

Например, если даны:

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 у каждого наблюдателя. Такое разделение упрощает добавление новых типов наблюдателей без изменения кода Stock!

Попробуйте сами

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: Создать новый Stock используя конструктор NewStock

	// 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 threshold, создать PriceAlert
		// - Для "logger": использовать configValue как LogName, создать PriceLogger
		// TODO: Подписать наблюдателя на stock
	}

	// Прочитать новую цену
	newPriceStr, _ := reader.ReadString('\n')
	newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)

	// TODO: Установить новую цену и получить ответы

	// TODO: Вывести каждый ответ на отдельной строке
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор Go