Готовый перевод Game Programming Patterns / Паттерны программирования в играх: 1.3. Observer
Куда ни плюнь, повсюду приложения создаются с использование схемы MVC(Модель-Предстваление-Контроллер), в основе которой лежит паттерн Observer(Наблюдатель).
Observer настолько популярен, что в языке Java он доступен из коробки (java.util.Observer), а в C# его встроили прямо в язык (в качестве ключевого слова).
-
-
-
Как и многие иные вещи в программировании, паттерн MVC был придуман Smalltalk-программистами в 70-х.
Программисты языка Lisp, возможно, заявляют, что они придумали это в 1960-х годах, но не побеспокоились записать на бумаге.
Паттерн Observer один из наиболее широко распространенных и часто используемых оригинальных паттернов "Банды Четрырех", однако, мир игростроя временами бывает настолько погружен в себя, что для его обитателей все они могут быть в новинку.
Поскольку, надеюсь, вы не с луны свалились, позвольте продемонстрировать вам мотивирующий пример.
Получено достижение
Допустим, вы добавляете систему достижений в свою игру.
Она будет включать в себя уйму разных значков, зарабатываемых игроками за завершение определенных этапов игры, как, например: "Убить 100 Обезъянодемонов", "Упасть с моста", или "Пройти уровень, используя только Мертвого Хорька".
Клянусь, здесь нет двусмысленностей.
Это довольно сложно сделать, так как у вас такой большой набор достижений, которые открываются всеми мыслимыми и немыслимыми способами.
Если не быть аккуратным, то щупальца вашей системы достижений проникнут в каждый уголок вашего кода.
Разумеется, достижение "Упасть с моста" как-то связано с физическим движком, но разве видеть вызов метода "открытьДостижениеПриПаденииСМоста()" прямо посреди вычислений в алгоритме столкновений это действительно то, что вы хотите?
Это риторический вопрос.
Ни один уважающий себя программист физики никогда не позволит заляпать свои прекрасные математические расчёты чем-то таким прозаичным, как элемент геймплея.
Что мы действительно бы хотели, это, как всегда, иметь весь код, относящийся к одному аспекту игры красиво собранным в одном месте.
Проблема в том, что достижения срабатывают от совокупности различных игровых аспектов.
Как это может работать без слияния кода достижений со всем остальным?
Для этого предназначен паттерн "Наблюдатель" (Observer).
С его помощью код объявляет о том, что случилось что-то интересное, не заботясь о получателе уведомления.
К примеру, у вас есть какой-то код физики, который управляет гравитацией и следит, кто отдыхает на удобных плоских поверхностях, а кто падает навстречу неминуемой гибели.
Чтобы отследить достижение "Упасть с моста", вы можете поместить код проверки прямо сюда - но это бардак.
Вместо этого можно просто сделать:
void Physics::updateBody(PhysicsBody& body)
{
bool wasOnSurface = body.
isOnSurface();
body.
accelerate(GRAVITY)
;
body.
update();
if (wasOnSurface && !body.
isOnSurface())
{
notify(body, EVENT_START_FALL)
;
}
}
Все, что этот код делает, это говорит: "Эм, не знаю кого это заботит, но эта штука только что упала.
Делай, что с сочтешь нужным."
Физический движок все же должен решить, какое сообщение отправить, то есть он не полностью отделен.
Но при проектировании мы чаще стараемся сделать системы лучше, а не совершенными.
Система достижений регистрирует себя, так что каждый раз при отправке физическим движком сообщения она получает его.
Потом она может проверить является ли падающее тело нашим слегка неуклюжим героем, и возвышался ли он на мосту непосредственно перед неприятным взаимодействием с законами классической механики.
Если да, она открывает соответствующее достижение с сопутствующими фейерверками и фанфарами, и все это не затрагивая код физики.
Фактически, вы можете менять набор достижений или целиком убрать систему достижений, не касаясь ни единой строчки кода физического движка.
Он и дальше будет отправлять сообщения, несмотря на факт того, что никто их больше не получает.
Конечно, если вы навсегда уберете достижения и никто и никогда больше не будет принимать сообщения физического движка, вы можете также убрать и код оповещений.
Но в процессе эволюции игры неплохо иметь такую гибкость.
Как это работает
Если вы все еще не знаете, как реализовать паттерн, то, возможно, догадаетесь из предыдущего описания, но для облегчения задачи я быстро рассмотрю этот вопрос.
The Observer (Наблюдатель)
Мы начнем с любопытствующего класса, который хочет знать, когда другой объект делает что-то интересное.
Этому классу соответствует код:
class Observer
{
public:
virtual ~Observer() {}
virtual void onNotify(const Entity& entity, Event event)
= 0;
};
Параметры для метода onNotify() остаются за вами.
Вот почему это паттерн "Наблюдатель", а не "готовый код-наблюдатель, который можно вставить в игру". Обычные параметры - объект, пославший уведомление, и настраиваемый параметр "data", в котором хранятся остальные данные.
Если вы пишете на языке с обобщениями(generics) или шаблонами(templates), то, возможно, будете их использовать, но также неплохо связать их с вашим конкретный случаем.
Здесь я просто заставлю его принимать игровую сущность и перечисление, описывающее случившееся.
Любой класс, наследующий этот, становится наблюдателем.
В нашем примере это система достижений, так что у нас получится нечто вроде:
class Achievements : public Observer
{
public:
virtual void onNotify(const Entity& entity, Event event)
{
switch (event)
{
case EVENT_ENTITY_FELL:
if (entity.
isHero() && heroIsOnBridge_)
{
unlock(ACHIEVEMENT_FELL_OFF_BRIDGE)
;
}
break;
// Обрабатываем остальные события и обновляем herolsOnBridge_...
}
}
private:
void unlock(Achievement achievement)
{
// Разблокировать, если еще не разблокировано...
}
bool heroIsOnBridge_;
};
Субъект
Метод сообщения вызывается объектом, за которым наблюдают.
Или если говорить языком "Банды Четырёх", субъектом. У него две цели.
Первая - это хранить список наблюдателей которые так терпеливо ждут его послания:
class Subject
{
private:
Observer* observers_[MAX_OBSERVERS];
int numObservers_;
};
В реальном коде вам лучше использовать динамическую коллекцию вместо простого массива.
Я описываю основы для тех людей, что пришли из других языков и не знают стандартную библиотеку C++.
Важным моментом является то, что субъект предоставляет публичное API для изменения этого списка.
class Subject
{
public:
void addObserver(Observer* observer)
{
// Добавить в список...
}
void removeObserver(Observer* observer)
{
// Удалить из списка...
}
// Все остальное...
};
Это позволяет контролировать, кто получает сообщения, вне этого кода.
Субъект общается с наблюдателями, но не связан с ними.
В нашем примере ни одна строчка физического кода не говорит о наличии достижений.
Тем не менее, он все еще оповещает систему достижений.
Это очень умно со стороны шаблона.
Также важно, что субъект имеет список наблюдателей вместо одного-единственного.
Это говорит нам о том, что наблюдатели завязаны явно друг на друга.
К примеру, аудио движок также слушает событие падения, чтобы проиграть соответствующий звук.
Если бы субъект поддерживал только одного наблюдателя, то аудио движок заменил бы систему достижений.
Это значит, что две системы вмешивались бы в работу друг друга самым неприятным образом, ибо одна отключала бы другую.
Поддержка списка наблюдателей гарантирует, что каждый наблюдатель обрабатывается независимо от других.
Каждый из них думает, что он единственный в мире следит за субъектом.
Другой задачей субъекта является отправка оповещений:
class Subject
{
protected:
void notify(const Entity& entity, Event event)
{
for (int i = 0; i < numObservers_; i++)
{
observers_[i]->onNotify(entity, event)
;
}
}
// Все остальное...
};
Заметьте, этот код предполагает, что наблюдатели не будут менять список в процессе вызова onNotify().
Более надежным решением будет запрещать или изящно обрабатывать изменения такого рода.
Физика для наблюдателей
Сейчас нам просто надо подключить все это в физический движок, чтобы он смог отправлять оповещения, а система достижений могла регистрировать себя для их получения.
Мы будем придерживаться оригинального рецепта из "Паттернов проектирования" и пронаследуем Subject:
class Physics : public Subject
{
public:
void updateBody(PhysicsBody& body)
;
};
Это позволит нам сделать метод notify() в Subject защищенным.
Таким образом пронаследованный класс физического движка может вызывать его для отправки оповещений, а код вне движка - нет.
Однако, методы addObserver() и removeObserver() остаются публичными, так что любой, кому доступна физическая система может наблюдать за ней.
Будь это настоящим кодом, я бы избегал наследования.
Вместо этого я бы создал экземпляр Subject в классе Physics.
А вместо наблюдения непосредственно за движком, субъектом был бы отдельный объект "событие падения".
Наблюдатели регистрировали бы себя следующим образом:
physics.
entityFell()
.addObserver(this)
;
По мне, в этом и заключается разница между системой "наблюдателей" и системой "событий".
Сперва вы наблюдаете за вещью, которая делает нечто интересное.
Потом вы уже наблюдаете за объектом, описывающим случившуюся вещь.
Теперь, когда физический движок совершает что-то, заслуживающее внимания, он вызывает notify() как в предыдущем примере.
Уведомления рассылаются по порядку всему списку наблюдателей.
Довольно просто, не находите?
Всего один класс, который обслуживает список указателей на объекты определенного интерфейса.
Трудно поверить, что нечто настолько прямолинейное является костяком взаимодействия бесчисленного количства программ и фреймворков.
Но и тут есть отрицательные стороны.
Когда я спросил других разработчиков игр, что они думают о паттерне, то услышал несколько общих жалоб.
Посмотрим, сможем ли мы сделать что-то для их решения.
"Он Слишком Медленный"
Я слышал это много раз, часто от программистов, которые в действительности не знают деталей реализации паттерна.
Это всего-навсего их предубеждение обо всем, похожем на "паттерн проектирования", что он включает горы классов, ссылок и других оригинальных способов растратить такты процессора.
Паттерн Observer получил особенно плохую репутацию среди них, потому что он яшкается с такими темными личностями, как "события", "сообщения" и даже "связывание данных". Некоторые из этих систем могут быть медленными (часто сознательно, и с благой целью).
Они включают в себя вещи, вроде очередей или динамического выделения памяти при каждом оповещении.
Поэтому важно документировать паттерны.
Когда мы путаемся в терминологии, мы теряем возможность разговаривать четко и ясно.
Вы говорите "Наблюдатель", а кто-то слышит "События" или "Сообщения", потому что либо не потрудились записать различия, либо не сочли нужным прочесть его.
Вот что я хочу сделать в этой книге.
Чтобы охватить основы, я также написал главу о событиях и сообщениях: "Очередь событий".
Но теперь, увидев реальную реализацию паттерна, вы знаете, что они тут не при чем.
Отправка оповещения - это всего лишь проход по списку и вызов некоторых виртуальных методов.
Конечно, это чуть медленнее, чем статически определенный метод, но эта цена мизерна по сравнению с наиболее критичному к производительности коду.
Паттерн идеально подходит для любого кода вне таких горячих участков, поэтому обычно можно использовать динамическую диспетчеризацию.
Кроме того, здесь, в действительности, нет накладных расходов.
Мы не выделяем память для сообщений.
Здесь нет очередей.
Это просто перенаправление через синхронный вызов метода.
Он настолько быстр?
На самом деле нужно соблюдать осторожность, потому что паттерн "Наблюдатель" - синхронный.
Субъект вызывает всех наблюдателей напрямую, что означает, пока все наблюдатели не закончат свои вызовы, он не вернется к своей работе.
Медленный наблюдатель может заблокировать субъекта.
Звучит страшно, на на практике это не конец света.
Это просто вещь, о которой вам следует быть в курсе.
Программисты пользовательского интерфейса (UI) — те, кто занимается событийно-ориентированным программированием, подобным этому, годами — имеют устоявшуюся мантру для этого: «держись подальше от UI-потока».
Если вы отвечаете на событие синхронно, вам нужно закончить и вернуть управление так быстро, как это возможно, чтобы UI не заблокировался.
Когда вам нужно сделать что-то тяжелое, выносите это в отдельный поток или очередь.
Однако вам нужно быть осторожным при использовании наблюдателей вместе с потоками и явными блокировками.
Если наблюдатель попытается захватить объект, уже захваченный субъектом, игра может зависнуть.
В движке с большим количеством потоков лучше использовать асинхронное взаимодействие с помощью "Очереди событий" (Event Queue).
"Слишком много динамического выделения"
Целые племена клана программистов - включая многих разработчиков игр - перешли на языки с автоматическим сбором мусора, поэтому динамическое выделение больше их не пугает.
Но для критичного к производительности ПО (такого, как игры) выделение памяти все еще важно даже в управляемых языках.
Динамическое выделение наряду с восстановлением памяти занимает время даже при автоматическом процессе.
Многие разработчики игр заботятся о выделении памяти меньше, чем о фрагментации.
Чтобы получить сертификат, игра должна проработать без падений несколько дней, а возрастающая фрагментация динамической области может помешать этому.
В главе "Объектный пул" (Object Pool) детальнее рассмотрен этот случай и обычная техника для его избежания.
В вышеприведенном примере кода использовался ограниченный массив в целях максимального упрощения.
В реальных реализациях список наблюдателей почти всегда является динамически выделяемой коллекцией, которая растет и уменьшается по мере добавления и удаления наблюдателей.
Такое месиво из памяти пугает некоторых.
Конечно, сразу понятно, что выделение памяти происходит только при привязке наблюдателей.
Отправка уведомления вообще не требует выделения памяти (это просто вызов метода).
Если привязать наблюдателей в начале игры и не слишком с ними возиться, количество выделенной памяти минимально.
Если это не помогло, я расскажу, как реализовать добавление и удаление наблюдателей без динамического выделения.
Связанные наблюдатели
В рассмотренном коде Subject имеет список указателей на каждого следящего Observer.
Класс Observer сам по себе не имеет ссылок на данный список.
Это чистый виртуальный интерфейс.
Интерфейсы предпочтительнее конкретных классов с состояниями, поэтому в общем тут все хорошо.
Но если хочется добавить состояние внутрь Observer, можно решить проблему с выделением памяти путем распараллеливания списка субъекта через самих наблюдателей.
Субъект больше не будет иметь отдельной коллекции указателей - вместо этого объекты наблюдателей станут узлами в связанном списке:
Для реализации этого сначала нужно избавиться от массива внутри Subject и заменить его указателем на голову списка наблюдателей:
class Subject
{
Subject()
: head_(NULL)
{}
// Методы...
private:
Observer* head_;
};
Дальше расширим Observer с помощью указателя на другого наблюдателя в списке:
class Observer
{
friend class Subject;
public:
Observer()
: next_(NULL)
{}
// Остальные данные...
private:
Observer* next_;
};
Также отмечаем Subject в качестве дружественного (friend).
У субъекта есть API для добавления и удаления наблюдателей, но теперь он будет управлять списком, находящимся внутри самого класса Observer.
Простейший способ разрешить ему тыкаться в список - сделать дружественным.
Регистрация нового наблюдателя теперь сводится к привязке его внутрь списка.
Воспользуемся легким вариантом, добавив его в начало:
void Subject::addObserver(Observer* observer)
{
observer->next_ = head_;
head_ = observer;
}
Другим вариантом будет добавление его в конец связанного списка.
Однако второй способ принесет некоторые сложности: Subject придется пройтись по списку до конца или хранить отдельный указатель tail_, всегда стоящий на последнем узле.
Добавление в начало списка проще, но имеет побочный эффект.
При прохождении по списку с целью отправки уведомления каждому наблюдателю, последний зарегистрированный наблюдатель получит оповещение первым.
Если зарегистрированы наблюдатели A, B и C (именно в таком порядке), то они получат уведомления в обратном порядке: C, B, A.
В теории это не важно.
Принцип правильного порядка наблюдателей в том, что два наблюдателя за одним субъектом не должны иметь зависимостей друг от друга.
Если порядок имеет значение, то два наблюдателя неуловимо связаны, что может закончиться плачевно.
Сделаем зачистку:
void Subject::removeObserver(Observer* observer)
{
if (head_ == observer)
{
head_ = observer->next_;
observer->next_ = NULL;
return;
}
Observer* current = head_;
while (current != NULL)
{
if (current->next_ == observer)
{
current->next_ = observer->next_;
observer->next_ = NULL;
return;
}
current = current->next_;
}
}
Код удаления узла из связанного списка обычно имеет кусок некрасивой обработки для удаления головы, который есть и здесь.
Есть и более элегантное решение с использованием указателя на указатель.
Здесь я его не показал, потому что каждый раз оно оказывалось не по зубам по крайней мере половине увидевших людей.
Однако это стоящее упражнение: помогает думать в терминах указателей.
В нашем односвязном списке нужно пройтись по порядку, чтобы найти наблюдателя, подлежащего удалению.
То же самое надо было бы сделать при использовании регулярного массива.
При использовании двусвязного списка, где каждый наблюдатель имеет указатель на узлы до и после него, можно удалить требуемый узел за фиксированное время.
В реальном коде я бы сделал так.
Осталось только отправить уведомление.
Это так же просто, как обход списка:
void Subject::notify(const Entity& entity, Event event)
{
Observer* observer = head_;
while (observer != NULL)
{
observer->onNotify(entity, event)
;
observer = observer->next_;
}
}
Обходим весь список и уведомляем каждого наблюдателя из него.
Теперь все наблюдатели имеют одинаковый приоритет и независимы друг от друга.
Можно настроить так, чтобы при уведомлении наблюдателя возвращался флаг, нужно ли субъекту продолжить обход списка.
Сделав это, максимально приблизимся к паттерну "Цепочка обязанностей" (Chain of Responsibility).
Неплохо, правда?
Субъект может иметь неограниченное количество наблюдателей без намека на динамическую память.
Регистрация и прекращение регистрации происходят так же быстро, как и в случае с простым массивом.
Хотя мы пожертвовали небольшой возможностью.
Объект наблюдателя в качестве узла списка может находиться только в одном списке субъекта.
Другими словами, наблюдатель может следить только за одним субъектом в данный промежуток времени.
В более традиционной реализации, где каждый субъект имеет свой независимый список, наблюдатель может находиться в нескольких списках одновременно.
Можно жить и с этим ограничением.
Естественнее для субъекта иметь нескольких наблюдателей, чем наоборот.
Но если это является проблемой, есть другое, более сложное решение, также не требующее динамического выделения.
Он не вместится в главу целиком, но я приведу отрывок, позволив самостоятельно заполнить пробелы.
Пул узлов списка
Как прежде, каждый субъект имеет связанный список наблюдателей.
Однако узлы этого списка не являются объектами наблюдателя.
Вместо этого, они представляют собой отдельные малые объекты "узла списка", содержащие указатель на наблюдателя и указатель на следующий узел в списке.
Из того, что несколько узлов могут указывать на одного наблюдателя, следует, что наблюдатель может находиться в нескольких списках субъекта в одно и то же время.
Теперь можно снова наблюдать за несколькими субъектами одновременно.
Связанные списки бывают двух видов.
В первом, который проходят в школе, есть узловой объект, содержащий данные.
В предыдущем примере со связанным наблюдателем было наоборот: данные (в данном случае, наблюдатель)
содержали узел (то
есть,
указатель next_).
Второй вид связанного списка называется "интрузивным", потому что использование объекта в списке вторгается в его определение.
Интрузивные списки менее гибкие, но, как мы видели, более эффективные.
Они распространены в таких местах, как ядро Linux, где этот компромисс имеет смысл.
Избежать динамического выделения просто: так как все узлы имеют одинаковый размер и тип, можно заранее выделить место под "Объектный пул" (Object Pool) из них.
Получается пачка узлов списка фиксированного размера, которую можно использовать, в том числе повторно, не трогая реальный распределитель памяти.
Оставшиеся проблемы
Думаю, мы прогнали двух главных монстров, пугающих людей в данном паттерне.
Как видно, паттерн простой, быстрый, его можно настроить для прекрасной работы с управлением памятью.
Но значит ли это, что нужно использовать наблюдателей все время?
Это уже другой вопрос.
Как и все паттерны проектирования, Наблюдатель - не серебряная пуля.
Даже когда он написан правильно и эффективно, его использование может быть ошибочным в целом.
Причиной, почему паттерны проектирования получили плохую репутацию, является использование их людьми в неподходящих местах, что только усиливает проблему.
Так, осталось всего две задачи, техническая и еще одна, относящаяся скорее к области применимости.
Разберемся с технической, потому что они всегда самые простые.
Уничтожение субъектов и наблюдателей
Ранее мы привели образец вполне рабочего кода, но его побочные действия - это важный вопрос: что произойдет, когда вы удалите субъект или наблюдателя?
Если вы просто удалите какого-нибудь наблюдателя, субъект все еще может хранить ссылку на него.
Теперь это указатель на освобожденную память.
Когда этот субъект попытается отправить оповещение, ну... именно из-за подобных вещей люди ненавидят C++.
Не красиво показывать пальцем, но заметьте, что в "Паттернах Проектирования" такой случай вообще не рассмотрен.
Уничтожение субъекта в большинстве случаев проще, так как наблюдатель вообще не имеет на него ссылок.
Но даже тогда, утилизация тела и разума субъекта в мусоросборнике менеджера памяти может вызвать проблемы.
Эти наблюдатели будут вечно ждать новых оповещений, не зная, что этого больше никогда не произойдет.
Теперь они не наблюдатели, только думают так.
С этим можно справиться несколькими путями.
Самый простой - сделать все тоже самое и довести до ума.
Сообщить о своем удалении всем субъектам - это обязанность наблюдателя.
Как правило, наблюдатель знает, за какими субъектами он следит, поэтому не составит труда добавить в его деструктор вызов removeObserver().
Впрочем, сделать это также просто, как и забыть об этом.
Если вы не хотите оставлять наблюдателей с субъектом-призраком, то это легко поправить.
Просто дайте субъекту отправить одно оповещения "последнего вздоха" прямо перед уничтожением.
Таким образом все наблюдатели получат его и выполнят соответствующие действия.
Будут скорбеть, плакать, и все в таком духе.
Когда речь заходит о надежности, люди - ненадежны. Даже те из нас, кто провел достаточно времени в компании с компьютерами, чтобы впитать в себя их точность.
Потому то мы и изобрели компьютеры: они не делают тех ошибок, которые совершаем мы.
Более безопасный подход состоит в том, чтобы наблюдатель автоматически удалял себя сам из всех субъектов, когда он уничтожается.
Включите это в логику базового класса наблюдателя, и любой, кто захочет его использовать, не будет должен помнить об этом.
Это добавит, конечно, некоторые сложности.
Фактически, каждому наблюдателю необходим список субъектов, за которыми он следит.
В конечном счете вы придете к указателям с обеих сторон.
Не волнуйтесь, я использую "сборщик мусора".
Все эти крутые детишки со своими клёвыми современными языками, использующие сборщик мусора, очень самодовольны сейчас.
Думаете, вы не должны беспокоится об этом, поскольку никогда явно не удалите ничего?
Подумайте еще раз!
Представьте себе: у вас есть интерфейс на экране, на котором показана статистика игрока: уровень здоровья и прочее.
Когда игрок вызывает его, вы создаете для него новый объект.
Когда интерфейс закрывается, вы просто передаете объект сборщику мусора для очистки и забываете о нем.
Каждый раз, когда персонаж получает по лицу (или по чему-нибудь еще), он посылает уведомление.
-
Экранный интерфейс получает его и обновляет уровень здоровья.
Отлично.
Что же случится, когда игрок закроет интерфейс, а вы, при этом, забудете отменить наблюдение?
Интерфейс больше не отображается, но не может быть удален сборщиком мусора, поскольку наблюдатель за персонажем все еще имеет ссылку на него.
Каждый раз, когда интерфейс загружается, мы добавляем его новый экземпляр во все более длинный список.
И все это время, пока игрок бегает, участвует в драках, в общем находится в игре, его персонаж посылает уведомления, которые получают все интерфейсы.
Они уже давно не показываются на экране, но все еще живы и тратят ресурсы процессора, обновляя невидимые игроку элементы.
Если они занимаются другими вещами, например, проигрывают звуки, вы заметите неправильное поведение.
Это часто встречающаяся проблема в системах уведомлений, поэтому у нее есть имя: "проблема бывшего слушателя" (Lapsed Listener Problem).
Хранение в субъектах ссылок на их слушателей может привести вас к немалому числу зомби-объектов, задержавшихся в памяти.
Усвойте урок: необходимо очень ответственно подходить к разрыву связей между объектами.
Как доказательство важности этой проблемы: о ней есть статья в Википедии (http://en.wikipedia.org/wiki/Lapsed_listener_problem)
Что происходит?
Другая, более глубокая проблема паттерна Наблюдатель вытекает из его прямого назначения.
Мы используем его, потому что он помогает ослабить связь между двумя частями кода.
Он позволяет субъекту неявно взаимодействовать с каким-нибудь наблюдателем, без статической привязки к нему.
Если задуматься о поведение субъекта - это окажется реальным достижением, что все эти раздражающие нахлебники будут отделены.
Когда вы ковыряетесь в физическом движке, вы действительно не хотите, что бы ваш редактор (и разум) был загромождён кучей вещей связанных с достижениями.
С другой стороны, если ваша программа не работает и баг закрался в цепочку наблюдателей, размышлять о последовательности связей будет куда сложнее.
Используя явную связь, куда проще найти вызываемый метод.
Детская забава для вашей IDE, если связи статические.
Но если связывание происходит через список наблюдателей, единственный способ понять, кто будет получать уведомление - проверить список наблюдателей во время работы программы.
Вместо того, чтобы в статическом случае рассуждать о структуре взаимодействия, вам придётся думать о её динамическом поведение.
Моё руководство как поступить с этим очень просто.
Если вам приходиться часто думать о взаимодействии обеих сторон, что бы разобраться в участке программы - не используйте шаблон "Наблюдатель" для описания подобной связи.
Предпочтите что-то более явное.
При разработке большой программой, Вы, скорее всего, разделяете её на отдельные куски, которые работают вместе.
Мы имеем множество терминов для этого: "распределение обязанностей", "связность и связанность", "модульность". Но всё сводится к одному - "эти штуки работают вместе, и не касаются вот этих штук".
Шаблон "Наблюдатель" отличный способ, чтобы несвязанные части общались между собой, без превращения всего в один огромный комок.
Он не так полезен в одной части кода, отвечающей за единый функциональный аспект.
Вот почему он уместен в нашем примере: достижения и физика почти полностью независимые области, скорее всего, разрабатываемые разными людьми.
Мы хотим минимизировать взаимодействие между ними таким образом, что бы работа каждой из них не требовала много знаний о другой.
"Наблюдатели" сегодня
Шаблоны проектирования берут свое начало в 90х.
Тогда объектно-ориентированное программирование было в тренде.
Каждый программист на Земле мечтал "Выучить ООП за 30 дней", а менеджеры платили им исходя их количества написанных классов.
Инженеры оценивали свое мастерство по глубине их наследственных иерархий.
В тот же год у Ace of Base вышло аж целых 3 хита. Думаю, это что-то да говорит о наших вкусах и взглядах на тот момент.
Шаблон "Наблюдатель" получил популярность в духе того времени, так что не удивительно, что он достаточно сложен.
Но современные программисты чувствуют себя более комфортно, используя функциональное программирование.
Реализовывать весь интерфейс только ради получения уведомлений не соответствует нынешней эстетике.
Это выглядит тяжеловесно и косно.
Это действительно тяжеловесно и косно!
Например, вы не можете иметь один класс, использующий разные методы обработки уведомлений для разных субъектов.
Поэтому субъект передаёт себя наблюдателю.
Так как наблюдатель имеет только один метод для обработки уведомлений, если он наблюдает за несколькими субъектами, он должен быть в состоянии определить какой из них вызвал его.
Более современный подход к "Наблюдателю" это просто ссылка на метод или функцию.
В языках с функциями первого класса, и в особенности с замыканиями, это более распространённый вариант реализации наблюдателя.
В наши дни практически в каждом языке есть замыкания.
C++ преодолел проблему замыканий в языках без сборщика мусора, и даже Java наконец-то урегулировала всё и ввела их в JDK8.
К примеру, в C# есть "события" вшитые в язык.
При этом, наблюдатель регистрируется как "делегат", что в терминах языка означает ссылку на метод.
В системе сообщений JavaScript, наблюдателем могут быть объекты, поддерживающие специальный протокол EventListener, или же просто функции.
Люди почти всегда используют последний вариант.
Если бы я проектировал "Наблюдателя" сегодня, я бы сделал его основанным на функциях, а не на классах.
Даже в C++, я бы стремился к системе, которая позволяет регистрировать указатели на функции как наблюдателя вместо экземпляра некого интерфейса Наблюдателя.
Вот интересный пост о способе реализовать это в C++.
"Наблюдатели" завтра
Системы событий и другие шаблоны, подобные "Наблюдателю", невероятно распространены в наши дни.
Это избитый путь.
Но если вы напишите несколько крупных приложений, используя их, вы начнёте кое-что замечать.
Большая часть кода ваших наблюдателей выглядит одинаково.
Это выглядит примерно так:
Получить уведомление о том, что некоторые состояние изменилось.
Изменить некоторый элемент UI, чтобы отразить новое состояние.
Все эти, "Ой, у героя теперь 7 жизней?
Позвольте мне поставить ширину индикатора здоровья в 70 пикселей"
Со временем всё это утомляет.
Академики компьютерных наук и инженеры-программисты пытались устранить эту скуку долгое время.
Их попытки известны под разными именами: "Программирование потока данных", "Реактивное программирование" и прочие.
Хоть и были некоторые успехи, в определённых областях (обработка звука или проектирование микросхем), Святой Грааль так и не найден.
В тоже время, менее амбициозный подход начинает набирать обороты.
Многие недавние фреймворки используют "привязку данных".
В отличии от более радикальных моделей, привязка данных не пытается полностью устранить императивный код и не пытается построить ваше приложение вокруг огромного декларативного потока данных.
Она занимается тем, что автоматически настраивает UI элемент или вычисляет некое значение, чтобы отразить изменение некой величины.
Как и другие декларативные системы, привязка данных достаточно медленная и сложная, что бы поместиться внутри ядра игрового движка.
Но я был бы удивлён, если бы не увидел, как её начали применять в менее критичных областях игры, таких как UI.
Тем не менее, старый добрый шаблон "Наблюдатель" всё ещё ждёт нас.
Конечно, он не так интересен, как популярные техники, которым удаётся втиснуть в название сразу и "функциональное", и "реактивное", но он убийственно прост, и он работает.
Для меня это два самых важных критерия для решения.
Переведено на Нотабеноиде
http://notabenoid.org/book/51192/215212
Переводчики: kamillxiii, IngaJolly, GromHoll, RSlafka
GONGOTA, kSky7000, Mingun
Настройки
Готово:
