Демонстрация паттерна Наблюдатель (Observer) на примере игровой системы достижений. Проект показывает, как реализовать слабосвязанную архитектуру, где игровые события автоматически уведомляют заинтересованные компоненты (достижения, UI) без прямых зависимостей между ними.
Наблюдатель (Observer) — поведенческий паттерн, который определяет механизм подписки, позволяющий одним объектам (наблюдателям) следить и реагировать на события, происходящие в других объектах (издателях).
- Когда изменение состояния одного объекта требует изменения других, и вы не хотите, чтобы объекты знали друг о друге
- Когда количество наблюдателей может меняться во время выполнения
- Когда нужно реализовать систему событий с минимальной связанностью
- Системы достижений
- Обновление UI при изменении здоровья/опыта
- Звуковые уведомления
- Системы квестов
┌─────────────────────────────────────────────────────────────────────────────┐
│ ПАТТЕРН НАБЛЮДАТЕЛЬ │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ ИЗДАТЕЛЬ │ │ НАБЛЮДАТЕЛЬ │ │
│ │ (EventManager) │ │ (IObserver) │ │
│ │ │ │ │ │
│ │ - subscribers │◄──────────────────────│ + OnNotify(data) │ │
│ │ + Subscribe() │ подписка └───────────┬─────────────┘ │
│ │ + Unsubscribe() │ ▲ │
│ │ + Notify() │ │ │
│ └──────────┬──────────┘ │ реализует │
│ │ │ │
│ │ уведомление ┌────────────┴─────────────┐ │
│ ▼ │ │ │
│ ┌──────────────────────────────────────────┐ │ ┌─────────────────────┐ │ │
│ │ ИГРОВЫЕ СОБЫТИЯ │ │ │ ICompletable │ │ │
│ │ │ │ │ Observer │ │ │
│ │ ┌────────────────────────────────────┐ │ │ │ │ │ │
│ │ │ GameEventType.MonsterKilled │ │ │ │ + IsCompleted {get} │ │ │
│ │ │ GameEventType.ItemCollected │ │ │ └──────────┬──────────┘ │ │
│ │ │ GameEventType.LevelCompleted │ │ │ ▲ │ │
│ │ └────────────────────────────────────┘ │ │ │ │ │
│ └──────────────────────────────────────────┘ │ ┌────────┴────────┐ │ │
│ │ │ Достижения │ │ │
│ │ │ │ │ │
│ │ │ • FirstBlood │ │ │
│ │ │ • Kill10Monsters│ │ │
│ │ │ • Collect3Items │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ UIPopupObserver │ │ │
│ │ │ (обычный) │ │ │
│ │ └─────────────────────┘ │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
ПОТОК ДАННЫХ
┌──────────┐ ┌─────────────────┐ ┌─────────────────────────────┐
│ Игрок │────▶│ EventManager │────▶│ Все подписанные наблюдатели │
│ убивает │ │ .Notify() │ │ получают OnNotify(eventData)│
│ монстра │ └─────────────────┘ └─────────────────────────────┘
└──────────┘ │
▼
┌───────────────────────────────────┐
│ Kill10MonstersAchievement │
│ progress += 1 │
│ if progress >= 10 → получено! │
└───────────────────────────────────┘
┌───────────────────────────────────┐
│ FirstBloodAchievement │
│ → получено! (первое убийство) │
└───────────────────────────────────┘
┌───────────────────────────────────┐
│ UIPopupObserver │
│ → выводит уведомление в UI │
└───────────────────────────────────┘
PatternObserver/
├── Program.cs # Точка входа, демонстрация работы
└── GameEvents/
├── IObserver.cs # Базовый интерфейс наблюдателя
├── ICompletableObserver.cs # Наблюдатель с признаком завершения
├── EventManager.cs # Издатель (Subject)
├── GameEventType.cs # Типы игровых событий (enum)
├── GameEventData.cs # Данные о событии
├── Kill10MonstersAchievement.cs # Достижение "Истребитель"
├── FirstBloodAchievement.cs # Достижение "Первая кровь"
├── Collect3ItemsAchievement.cs # Достижение "Коллекционер"
└── UIPopupObserver.cs # UI уведомления
| Класс/Интерфейс | Описание |
|---|---|
IObserver |
Базовый интерфейс. Содержит метод OnNotify(GameEventData), который вызывается при наступлении события |
ICompletableObserver |
Расширяет IObserver. Добавляет свойство IsCompleted для наблюдателей, которые могут завершить свою работу |
EventManager |
Центральный диспетчер событий. Управляет подписками и уведомляет наблюдателей о событиях |
GameEventType |
Перечисление типов событий: MonsterKilled, ItemCollected, LevelCompleted |
GameEventData |
DTO с данными о событии: тип, имя цели, значение (количество) |
Kill10MonstersAchievement |
Достижение "Истребитель" — активируется после убийства 10 монстров |
FirstBloodAchievement |
Достижение "Первая кровь" — активируется после первого убийства |
Collect3ItemsAchievement |
Достижение "Коллекционер" — активируется после сбора 3 предметов |
UIPopupObserver |
Простой наблюдатель, выводящий все события в консоль (имитация UI) |
EventManager manager = new EventManager();
var killAchievement = new Kill10MonstersAchievement();
var uiPopup = new UIPopupObserver();
manager.Subscribe(GameEventType.MonsterKilled, killAchievement);
manager.Subscribe(GameEventType.MonsterKilled, uiPopup);manager.Notify(new GameEventData
{
Type = GameEventType.MonsterKilled,
TargetName = "Гоблин",
Value = 1
});Kill10MonstersAchievementувеличивает внутренний счетчикUIPopupObserverвыводит уведомление в консоль
Когда достижение выполнено (IsCompleted == true), EventManager автоматически удаляет его из списка подписчиков. Это предотвращает:
- Лишние проверки в завершенных достижениях
- Утечки памяти (если бы наблюдатели хранились вечно)
public enum GameEventType
{
MonsterKilled,
ItemCollected,
LevelCompleted,
BossDefeated // Новое событие
}// 1. Создать класс, реализующий ICompletableObserver
public class KillBossAchievement : ICompletableObserver
{
public bool IsCompleted { get; private set; }
public void OnNotify(GameEventData eventData)
{
if (IsCompleted) return;
if (eventData.Type != GameEventType.BossDefeated) return;
IsCompleted = true;
Console.WriteLine("Достижение получено: Победитель боссов");
}
}
// 2. Подписать в Program.cs
manager.Subscribe(GameEventType.BossDefeated, new KillBossAchievement());public class LoggerObserver : IObserver
{
public void OnNotify(GameEventData eventData)
{
File.AppendAllText("game.log", $"{DateTime.Now}: {eventData.Type}\n");
}
}| Принцип | Как применен |
|---|---|
Single Responsibility |
EventManager управляет подписками, каждое достижение отвечает только за свою логику |
Open/Closed |
Новые достижения добавляются без изменения существующего кода |
Liskov Substitution |
Любой ICompletableObserver может использоваться как IObserver |
Interface Segregation |
Разделены IObserver и ICompletableObserver — UI не реализует ненужные методы |
Dependency Inversion |
Наблюдатели зависят от абстракций (IObserver), а не от конкретных классов |
- Слабая связанность — издатель не знает о конкретных наблюдателях
- Расширяемость — новые достижения добавляются без изменения существующего кода
- Безопасная отписка — завершенные наблюдатели автоматически удаляются
- Типобезопасность — события типизированы через
GameEventTypeиGameEventData
Vladimir Vaize | GitHub | Telegram Channel