Паттерн 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.
Задание
ЛегкоДавайте создадим систему оповещения о ценах на акции, используя паттерн Наблюдатель (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: Выведите каждый ответ на отдельной строке
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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