Паттерн «Наблюдатель»
Часть раздела Объектно-ориентированное программирование путешествия по GDScript на Coddy. Урок 48 из 65.
Паттерн Observer позволяет одному объекту, субъекту, сообщать множеству других объектов, наблюдателям, что что-то произошло. Субъект не знает, что наблюдатели делают с этой информацией; он лишь сообщает о событии.
При написании вручную субъект хранит массив наблюдателей и вызывает один и тот же метод для каждого из них:
# health.gd
class_name Health
extends RefCounted
var observers: Array = []
var hp := 100
func add_observer(observer) -> void:
observers.append(observer)
func take_damage(amount: int) -> void:
hp -= amount
for observer in observers:
observer.on_hp_changed(hp)В Godot этот шаблон уже встроен. Сигнал — это объявление субъекта, а каждый вызов connect() добавляет наблюдателя, поэтому массив и цикл больше не нужны:
# health.gd
class_name Health
extends RefCounted
signal hp_changed(new_hp: int)
var hp := 100
func take_damage(amount: int) -> void:
hp -= amount
hp_changed.emit(hp)Наблюдателем может быть любой объект с методом, параметры которого соответствуют сигналу:
# health_bar.gd
class_name HealthBar
extends RefCounted
func on_hp_changed(new_hp: int) -> void:
print("Bar: ", new_hp, "/100")# damage_log.gd
class_name DamageLog
extends RefCounted
func on_hp_changed(new_hp: int) -> void:
if new_hp < 50:
print("Log: low health!")# main.gd
var health := Health.new()
var bar := HealthBar.new()
var damage_log := DamageLog.new()
health.hp_changed.connect(bar.on_hp_changed)
health.hp_changed.connect(damage_log.on_hp_changed)
health.take_damage(70)Bar: 30/100
Log: low health!Health понятия не имеет о существовании полосы или журнала. Звуковой эффект или достижение тоже могут подписаться на событие, и для этого не нужно вносить ни единого изменения в health.gd.
Наблюдатели работают, если соблюдать три правила:
- Подключать один и тот же метод дважды нельзя. Если такое может произойти, сначала проверь с помощью
hp_changed.is_connected(bar.on_hp_changed).disconnect()удаляет наблюдателя, и отключение того, который не подключён, тоже является ошибкой. - Подключение не удерживает наблюдателя в памяти. Храни наблюдателя в переменной или массиве: объект, созданный внутри вызова
connect(), сразу освобождается и никогда не получает сигнал. - Подключай методы наблюдателей. Лямбда-функция, которая использует субъект и подключена к собственному сигналу субъекта, удерживает субъект в памяти навсегда. Godot сообщает об утечке при закрытии игры.
Задание
СреднеСоздай метеостанцию, которая уведомляет свои дисплеи с помощью сигнала.
В weather_station.gd:
- Объяви сигнал
temperature_changed(value: float). attach(display)подключает метод дисплеяon_temperature_changed. Повторное подключение уже подключённого дисплея ничего не делает.detach(display)отключает его. Отключение дисплея, который не подключён, ничего не делает.set_temperature(value)сохраняет значение вtemperatureи отправляет его вместе с сигналом.
В weather_display.gd метод on_temperature_changed(value) выводит имя дисплея и значение с одним знаком после десятичной запятой, например Phone: 21.5 C.
Предоставленный код считывает имена дисплеев в одной строке, затем количество команд и по одной команде в каждой следующей строке: attach NAME, detach NAME или temp VALUE. В конце он выводит последнюю температуру. Для имён Phone Tablet и команд attach Phone, attach Tablet, temp 21.5, detach Phone, temp 19 результат будет таким:
Phone: 21.5 C
Tablet: 21.5 C
Tablet: 19.0 C
Final temperature: 19.0Твой код должен находиться в weather_station.gd и weather_display.gd. В main.gd находится предоставленный код ввода-вывода, и его нельзя редактировать.
Попробуйте сами
extends Node
func _ready():
# Предоставленный код ввода/вывода: оставьте его без изменений
var station := WeatherStation.new()
var displays := {}
for display_name in OS.read_string_from_stdin().split(" "):
displays[display_name] = WeatherDisplay.new(display_name)
var count := int(OS.read_string_from_stdin())
for i in count:
var parts := OS.read_string_from_stdin().split(" ")
match parts[0]:
"attach":
station.attach(displays[parts[1]])
"detach":
station.detach(displays[parts[1]])
"temp":
station.set_temperature(float(parts[1]))
print("Final temperature: %.1f" % station.temperature)
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыВведение в ООПКлассы и объектыКлючевое слово selfМетодыСвойстваКонструктор (_init)Повторение: простой калькулятор4Полиморфизм
Переопределение методов: повторениеУтиная типизацияАбстрактные классы и методыПроектирование интерфейсовПовторение — калькулятор фигур7Сигналы
Что такое сигнал?Объявление и отправкаПодключение к сигналамСигналы с аргументамиПовторение — система здоровья10Паттерны проектирования. Часть 2
Паттерн «Команда»Паттерн «Адаптер»Паттерн «Декоратор»Паттерн «Шаблонный метод»Паттерн «Состояние»Паттерн «Компоновщик»2Свойства классов
Переменные экземпляра и классаСтатические функцииСеттеры свойствГеттеры свойствПриватность по соглашениюПовторение — менеджер банковского счёта5Инкапсуляция
Открытые и закрытые членыПроверка данных с помощью сеттеровСокрытие информацииСвойства только для чтенияПовторение — система учета студентов8Продвинутые концепции ООП
Композиция и наследованиеВнутренние классыУзлы как компонентыRefCounted и NodeПеречисления в классах11Проект: Управление библиотекой
Обзор проектаКлассы книг и пользователей3Наследование
Основы наследованияКлючевое слово superПереопределение методовЦепочки наследованияПроверка типов с помощью isПовторение — иерархия сотрудников6Специальные методы
Введение в виртуальные методыМетод _to_stringПользовательские итераторыПовторение: пользовательский список9Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн «Одиночка»Паттерн «Фабрика»Паттерн «Наблюдатель»Паттерн «Стратегия»Потренируйтесь самостоятельно: Онлайн-компилятор GDScript