Паттерн Observer (События)
Часть раздела Объектно-ориентированное программирование путешествия по C# на Coddy — урок 54 из 70.
Паттерн «Наблюдатель» (Observer pattern) устанавливает связь «один ко многим», при которой один объект (субъект) уведомляет множество зависимых объектов (наблюдателей) об изменении своего состояния. В C# события предоставляют встроенный способ элегантной реализации этого паттерна.
Этот паттерн включает в себя издателя, который генерирует события, и подписчиков, которые на них реагируют. Вот практический пример монитора цен на акции:
public class Stock
{
private decimal _price;
public event Action<decimal> PriceChanged;
public decimal Price
{
get => _price;
set
{
_price = value;
PriceChanged?.Invoke(_price); // Уведомить всех наблюдателей
}
}
}
public class PriceAlert
{
public void OnPriceChanged(decimal newPrice)
{
Console.WriteLine($"Alert: Price is now {newPrice}");
}
}
public class PriceLogger
{
public void OnPriceChanged(decimal newPrice)
{
Console.WriteLine($"Logged: {newPrice}");
}
}Подписчики прикрепляют свои методы к событию с помощью += и могут открепить их с помощью -=:
var stock = new Stock();
var alert = new PriceAlert();
var logger = new PriceLogger();
stock.PriceChanged += alert.OnPriceChanged;
stock.PriceChanged += logger.OnPriceChanged;
stock.Price = 150.50m;
// Вывод:
// Оповещение: Цена теперь 150.50
// Записано в лог: 150.50
stock.PriceChanged -= logger.OnPriceChanged; // Отписаться
stock.Price = 155.00m;
// Вывод: Оповещение: Цена теперь 155.00Паттерн Наблюдатель (Observer) способствует слабой связанности — классу Stock не нужно ничего знать о своих наблюдателях. Новые наблюдатели могут подписываться без изменения издателя, что делает систему простой в расширении и обслуживании.
Задание
ЛегкоДавайте создадим систему мониторинга температуры, используя паттерн «Наблюдатель» (Observer) с помощью событий. Вы создадите датчик, который уведомляет нескольких наблюдателей при каждом изменении температуры — это идеально подходит для таких сценариев, как системы климат-контроля, где различные компоненты должны реагировать на обновления температуры.
Вы организуете свой код в трех файлах:
TemperatureSensor.cs: Создайте классTemperatureSensorв пространстве именMonitoring. Это ваш издатель — субъект, за которым будут наблюдать другие объекты. Ваш датчик должен иметь:- Закрытое поле для хранения текущей температуры
- Событие с именем
TemperatureChanged, которое используетAction<double>для уведомления подписчиков о новой температуре - Свойство
Temperature, которое инициирует событие всякий раз, когда температуре присваивается новое значение
Observers.cs: Создайте два класса наблюдателей в том же пространстве имен, которые будут подписываться на изменения температуры:DisplayUnit— имеет методOnTemperatureChanged(double temp), который выводитDisplay: {temp}CAlarmSystem— имеет методOnTemperatureChanged(double temp), который выводитAlarm Check: {temp}C
Program.cs: Объедините все вместе, создав датчик и обоих наблюдателей. Подпишите обоих наблюдателей на событие датчика, затем обновите температуру. После этого отпишите систему сигнализации и снова обновите температуру, чтобы показать, что уведомление получает только дисплей.
Вы получите два входных значения:
- Первое показание температуры (например,
22.5) - Второе показание температуры (например,
28.0)
Ваша программа должна:
- Создать датчик и обоих наблюдателей
- Подписать обоих наблюдателей на событие
TemperatureChanged - Установить температуру датчика на первое входное значение (оба наблюдателя должны быть уведомлены)
- Отписать
AlarmSystemот события - Установить температуру датчика на второе входное значение (только дисплей должен быть уведомлен)
Например, если входные данные — 22.5 и 28.0, вывод должен быть следующим:
Display: 22.5C
Alarm Check: 22.5C
Display: 28CЭто демонстрирует основную мощь паттерна «Наблюдатель» — датчик не знает и не заботится о том, кто его слушает. Наблюдатели могут подписываться и отписываться динамически, а издатель просто уведомляет тех, кто слушает в данный момент!
Шпаргалка
Паттерн Наблюдатель (Observer) устанавливает связь «один ко многим», при которой один объект (субъект) уведомляет несколько зависимых объектов (наблюдателей) об изменении своего состояния.
В C# события предоставляют встроенный способ реализации этого паттерна. Паттерн включает в себя издателя (publisher), который инициирует события, и подписчиков (subscribers), которые на них реагируют.
Базовая реализация событий
Определите событие с помощью event Action<T> и вызывайте его при изменении состояния:
public class Stock
{
private decimal _price;
public event Action<decimal> PriceChanged;
public decimal Price
{
get => _price;
set
{
_price = value;
PriceChanged?.Invoke(_price); // Notify all observers
}
}
}Создание наблюдателей
Наблюдатели — это классы с методами, сигнатура которых соответствует событию:
public class PriceAlert
{
public void OnPriceChanged(decimal newPrice)
{
Console.WriteLine($"Alert: Price is now {newPrice}");
}
}
public class PriceLogger
{
public void OnPriceChanged(decimal newPrice)
{
Console.WriteLine($"Logged: {newPrice}");
}
}Подписка и отписка
Используйте += для подписки и -= для отписки от событий:
var stock = new Stock();
var alert = new PriceAlert();
var logger = new PriceLogger();
stock.PriceChanged += alert.OnPriceChanged; // Subscribe
stock.PriceChanged += logger.OnPriceChanged; // Subscribe
stock.Price = 150.50m; // Both observers notified
stock.PriceChanged -= logger.OnPriceChanged; // Unsubscribe
stock.Price = 155.00m; // Only alert notifiedПаттерн Наблюдатель способствует слабой связанности (loose coupling) — издателю не нужно ничего знать о своих наблюдателях. Новые наблюдатели могут подписываться без изменения кода издателя.
Попробуйте сами
using System;
using Monitoring;
class Program
{
public static void Main(string[] args)
{
// Считать входные температуры
double temp1 = Convert.ToDouble(Console.ReadLine());
double temp2 = Convert.ToDouble(Console.ReadLine());
// TODO: Создать экземпляр `TemperatureSensor`
// TODO: Создать экземпляры `DisplayUnit` и `AlarmSystem`
// TODO: Подписать обоих наблюдателей на событие `TemperatureChanged`
// TODO: Установить температуру датчика на `temp1` (оба наблюдателя уведомлены)
// TODO: Отписать `AlarmSystem` от события
// TODO: Установить температуру датчика на `temp2` (уведомлен только дисплей)
}
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыПространства имен и директивыВведение в классы и объектыКлючевое слово 'this'Методы и параметрыПоля и свойстваКонструкторыИнициализаторы объектовИтоги — Простой калькулятор4Наследование
Основы синтаксиса наследования (:)Ключевое слово 'base'Ключевые слова Virtual и OverrideЗапечатанные классыБазовый класс 'object'Итоги — Иерархия сотрудников7Продвинутые возможности
Перегрузка операторовИндексаторы (this[])Переопределение ToString()Методы расширенияПовторение — Пользовательский список10Паттерны проектирования. Часть 1
Введение в паттерны проектированияПотокобезопасный SingletonПаттерн FactoryПаттерн Observer (События)Паттерн Strategy13Финальные испытания
Платформа для онлайн-обученияКомпонент игрового персонажаСервис аренды транспорта2Свойства и статические члены
Автоматически реализуемые свойстваСвойства только для чтения и записиСтатические поля и методыСтатические классыЧлены в виде выражений5Полиморфизм и интерфейсы
Полиморфизм: компиляция vs выполнениеИнтерфейс vs абстрактный классМножественные интерфейсыЯвные интерфейсыUpcasting и DowncastingИтоги: Калькулятор фигур8Продвинутые концепции ООП
Композиция вместо наследованияGenerics (классы и методы)Делегаты и событияАтрибуты и рефлексияIDisposable и оператор usingОсновы Dependency Injection11Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн Компоновщик3Архитектура классов
Экземплярные и статические данныеКлючевые слова 'readonly' и 'const'Опорные поляПовторение — Менеджер банковских счетов6Инкапсуляция
Модификаторы доступаСвойства для инкапсуляцииРеализация скрытия данныхПаттерны неизменяемостиИтоги — Записи студентов9Переменное количество аргументов
Ключевое слово 'params'Необязательные параметрыИменованные аргументыПерегрузка методов