Menu
Coddy logo textTech

Паттерн 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 не нужно ничего знать о своих наблюдателях. Новые наблюдатели могут подписываться без изменения издателя, что делает систему простой в расширении и обслуживании.

challenge icon

Задание

Легко

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

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

  • TemperatureSensor.cs: Создайте класс TemperatureSensor в пространстве имен Monitoring. Это ваш издатель — субъект, за которым будут наблюдать другие объекты. Ваш датчик должен иметь:
    • Закрытое поле для хранения текущей температуры
    • Событие с именем TemperatureChanged, которое использует Action<double> для уведомления подписчиков о новой температуре
    • Свойство Temperature, которое инициирует событие всякий раз, когда температуре присваивается новое значение
  • Observers.cs: Создайте два класса наблюдателей в том же пространстве имен, которые будут подписываться на изменения температуры:
    • DisplayUnit — имеет метод OnTemperatureChanged(double temp), который выводит Display: {temp}C
    • AlarmSystem — имеет метод OnTemperatureChanged(double temp), который выводит Alarm Check: {temp}C
  • Program.cs: Объедините все вместе, создав датчик и обоих наблюдателей. Подпишите обоих наблюдателей на событие датчика, затем обновите температуру. После этого отпишите систему сигнализации и снова обновите температуру, чтобы показать, что уведомление получает только дисплей.

Вы получите два входных значения:

  • Первое показание температуры (например, 22.5)
  • Второе показание температуры (например, 28.0)

Ваша программа должна:

  1. Создать датчик и обоих наблюдателей
  2. Подписать обоих наблюдателей на событие TemperatureChanged
  3. Установить температуру датчика на первое входное значение (оба наблюдателя должны быть уведомлены)
  4. Отписать AlarmSystem от события
  5. Установить температуру датчика на второе входное значение (только дисплей должен быть уведомлен)

Например, если входные данные — 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` (уведомлен только дисплей)
    }
}
quiz iconПроверьте себя

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

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