Паттерн 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.
Задание
ЛегкоДавайте создадим систему оповещений об изменении цены акций с использованием паттерна 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: Вывести каждый ответ на отдельной строке
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker PoolПотренируйтесь самостоятельно: Онлайн-компилятор Go