Menu
Coddy logo textTech

Паттерн Observer

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

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

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

subject.Notify("Order placed")
// Email для user@example.com: Order placed
// Лог: Order placed

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

challenge icon

Задание

Легко

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

Вы организуете свой код в трех файлах:

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

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

    Реализуйте два типа наблюдателей:

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

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

    • 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 у каждого наблюдателя. Такая слабая связанность (decoupling) позволяет легко добавлять новые типы наблюдателей без изменения кода Stock!

Шпаргалка

Паттерн Наблюдатель (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)
}

Использование

Подпишите наблюдателей и уведомьте их об изменениях:

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

	// Чтение новой цены
	newPriceStr, _ := reader.ReadString('\n')
	newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)

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

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

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

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