Шаблоны программирования в разработке игр: «Наблюдатель» (лонг)

Позволяет событиям, зависящим от другого объекта, происходить без постоянной проверки его состояния, экономя ресурсы компьютера.

Шаблоны программирования в разработке игр: «Наблюдатель» (лонг)

Как и всегда, мы хотим, чтобы весь код, относящийся к одному аспекту игры, был аккуратно сгруппирован в одном месте. Проблема в том, что достижения могут разблокироваться из-за множества разных игровых событий. Как сделать так, чтобы это работало, не связывая код системы достижений со всеми этими событиями напрямую?

Для этого и существует паттерн «Наблюдатель». Он позволяет одной части кода объявить о том, что произошло что-то важное, не заботясь о том, кто именно получит это уведомление.

Например, у нас есть код физики, который обрабатывает гравитацию и отслеживает, какие объекты спокойно стоят на твердой поверхности, а какие летят навстречу неизбежной гибели. Чтобы реализовать достижение «Упал с моста», мы могли бы просто вставить код обработки достижения прямо в физический движок, но это было бы ужасно неаккуратно. Вместо этого мы можем сделать так:

void Physics::updateEntity(Entity& entity) { bool wasOnSurface = entity.isOnSurface(); entity.accelerate(GRAVITY); entity.update(); if (wasOnSurface && !entity.isOnSurface()) { notify(entity, EVENT_START_FALL); } }

Всё, что делает этот механизм, — это говорит: «Эм, я не знаю, важно ли это кому-то, но вот эта штука только что упала. Делайте с этим что хотите.»

Система достижений регистрирует себя как наблюдатель, чтобы получать уведомления, когда код физики сообщает о каких-либо событиях. Затем она проверяет, не является ли падающий объект нашим неуклюжим героем и стоял ли он до этого на мосту, прежде чем познакомиться с законами классической механики в самом неприятном их проявлении. Если да, система разблокирует соответствующее достижение с фейерверками и фанфарами — и всё это без какого-либо участия кода физического движка.

Более того, мы можем изменить список достижений или вовсе удалить всю систему достижений, не изменяя ни одной строчки в коде физики. Движок по-прежнему будет отправлять свои уведомления, даже если больше никто их не принимает.

Как это работает?

Если вы ещё не знаете, как реализовать этот паттерн, то, скорее всего, уже догадались из описания выше. Но чтобы упростить задачу, давайте быстро разберёмся.

Наблюдатель (Observer)

Начнём с любопытного класса, который хочет знать, когда другой объект делает что-то интересное. Эти любопытные объекты определяются следующим интерфейсом:

class Observer { public: virtual ~Observer() {} virtual void onNotify(const Entity& entity, Event event) = 0; };

Любой конкретный класс, который реализует этот интерфейс, становится наблюдателем. В нашем примере это система достижений, поэтому мы могли бы написать что-то вроде этого:

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; // Handle other events, and update heroIsOnBridge_... } } private: void unlock(Achievement achievement) { // Unlock if not already unlocked... } bool heroIsOnBridge_; };

Субъект (Subject)

Метод уведомления вызывается объектом, за которым ведётся наблюдение. В терминологии Gang of Four этот объект называется «субъектом». У него две задачи:

  • Хранить список наблюдателей, которые терпеливо ждут, когда он отправит им уведомление.
  • Отправлять уведомления наблюдателям, когда происходит какое-то значимое событие.

Пример кода для реализации субъекта:

class Subject { private: Observer* observers_[MAX_OBSERVERS]; int numObservers_; };

Ключевой момент в том, что субъект предоставляет публичный API для изменения этого списка.

class Subject { public: void addObserver(Observer* observer) { // Add to array... } void removeObserver(Observer* observer) { // Remove from array... } // Other stuff... };

Это позволяет внешнему коду управлять тем, кто получает уведомления. Субъект взаимодействует с наблюдателями, но не связан с ними напрямую.

В нашем примере код физического движка нигде не упоминает систему достижений. Тем не менее, он всё равно может с ней взаимодействовать. В этом и заключается хитрость этого паттерна.

Также важно, чтобы субъект хранил список наблюдателей, а не только одного. Это предотвращает скрытую зависимость наблюдателей друг от друга.

Например, представим, что аудиосистема тоже подписана на событие падения, чтобы проигрывать соответствующий звук. Если бы субъект поддерживал только одного наблюдателя, то при регистрации аудиосистемы она отменила бы регистрацию системы достижений.

Это привело бы к конфликту между этими двумя системами — и в особенно неприятной форме, ведь вторая система незаметно отключила бы первую.

Использование списка наблюдателей гарантирует, что каждый наблюдатель работает независимо. С его точки зрения, он единственный, кто следит за субъектом.

Вторая ключевая задача субъекта — отправка уведомлений:

class Subject { protected: void notify(const Entity& entity, Event event) { for (int i = 0; i < numObservers_; i++) { observers_[i]->onNotify(entity, event); } } // Other stuff... };

Наблюдаемая физика

Теперь нам нужно интегрировать всё это в физический движок, чтобы он мог отправлять уведомления, а система достижений могла подписываться на их получение. Мы будем придерживаться оригинального рецепта из Design Patterns и унаследуем Subject:

class Physics : public Subject { public: void updateEntity(Entity& entity); };

Это позволяет нам сделать метод notify() в Subject защищённым (protected). Таким образом, наследуемый класс физического движка может вызывать его для отправки уведомлений, но код за пределами этого класса не сможет делать то же самое. В то же время методы addObserver() и removeObserver() остаются публичными (public), так что любой код, имеющий доступ к физическому движку, может подписаться на его события.

В реальном коде я бы избегал наследования в этом случае. Вместо этого лучше сделать так, чтобы класс Physics содержал экземпляр Subject. Вместо наблюдения за самим физическим движком, субъектом будет отдельный объект события, например "событие падения".

Наблюдатели могли бы подписываться на него так:

physics.entityFell() .addObserver(this);

Для меня это и есть разница между системой наблюдателей (Observer) и системой событий (Event System):

  • В Observer-системе вы наблюдаете объект, который совершает что-то важное.
  • В Event-системе вы подписываетесь на событие, которое представляет это важное действие.

Теперь, когда физический движок делает что-то значимое, он просто вызывает notify(), как в примере ранее. Этот вызов проходит по списку наблюдателей и отправляет им всем уведомления.

Шаблоны программирования в разработке игр: «Наблюдатель» (лонг)

Довольно просто, правда?

Всего один класс, который хранит список указателей на экземпляры интерфейса. Трудно поверить, что настолько простой механизм является основой коммуникации в бесчисленных программах и фреймворках.

Но у паттерна Observer есть и критики. Когда я спрашивал других разработчиков игр, что они думают об этом паттерне, они называли несколько проблем. Давайте посмотрим, можно ли их как-то решить.

“Он слишком медленный”

Эту жалобу я слышу часто, причём чаще всего от программистов, которые не вдавались в детали работы паттерна. Они автоматически предполагают, что всё, что похоже на «паттерн проектирования», обязательно включает в себя кучу классов, лишние уровни абстракции и прочие хитроумные способы тратить ресурсы процессора.

Особенно Observer страдает от своей связи с такими концепциями, как “события” (events), “сообщения” (messages) и даже “data binding” (привязка данных). Некоторые из этих механизмов действительно могут быть медленными — и зачастую намеренно, поскольку их цель — реализовать гибкость через очереди или динамическое выделение памяти для каждого уведомления.

Именно поэтому я считаю, что важно чётко документировать паттерны. Когда мы размываем термины, теряется возможность ясно и кратко выражать мысли. Вы говорите “Observer”, а кто-то слышит “Event” или “Messaging”, потому что никто не удосужился записать разницу, или они просто не прочитали об этом.

Собственно, одна из целей этой книги — разъяснить такие нюансы. Я даже выделил отдельные главы про Event Queue и Messaging.

Но теперь, когда вы увидели, как работает Observer на практике, вы знаете, что он не тратит ресурсы зря. Отправка уведомления — это просто обход списка и вызов виртуального метода. Да, это немного медленнее, чем статический вызов функции, но эта разница незначительна для кода, который не находится в самых горячих точках производительности.

К тому же, этот паттерн обычно используется вне критических участков кода, так что небольшая потеря скорости — не проблема. Тем более что здесь нет выделения памяти, нет очередей — просто вызов метода через указатель.

“Он слишком быстрый?”

На самом деле, здесь даже можно столкнуться с обратной проблемой: Observer работает синхронно. Это значит, что субъект вызывает методы наблюдателей напрямую и не продолжает выполнение кода, пока все наблюдатели не завершат обработку уведомления.

Если один из наблюдателей работает медленно, он может заблокировать выполнение субъекта.

Звучит пугающе, но на практике это не такая уж большая проблема. Главное — осознавать этот нюанс. Разработчики UI, которые давно используют событийное программирование, знают правило:

“Не нагружайте UI-поток”

Если ваш обработчик события работает синхронно, он должен выполняться как можно быстрее, чтобы не блокировать остальные задачи. Если нужно сделать что-то тяжёлое, отправьте это в отдельный поток или в очередь задач.

Но будьте осторожны, если сочетаете наблюдателей с многопоточностью и блокировками. Если один из наблюдателей захватит лок, который удерживает субъект, это может застопорить всю игру (deadlock).

В многопоточных игровых движках вместо Observer может быть лучше использовать асинхронную систему событий (например, Event Queue).

“Он слишком много выделяет памяти”

Сегодня многие программисты, особенно работающие с управляемыми языками (C#, Java), не так переживают из-за динамического выделения памяти, как раньше. Но в игровой разработке это всё ещё критично:

  • Выделение памяти тратит время
  • Освобождение памяти (даже автоматическое) тоже нагружает систему
  • Фрагментация кучи может привести к проблемам, если игра должна работать сутками без сбоев

В своём примере я использовал фиксированный массив для простоты. Но в реальных системах список наблюдателей почти всегда динамически расширяется при добавлении новых подписчиков. Этот процесс может вызывать фрагментацию памяти, что пугает некоторых разработчиков.

Но здесь важно понимать:

  • Выделение памяти происходит только при подписке/отписке наблюдателей
  • Отправка уведомлений памяти не требует — это всего лишь вызовы методов
  • Если подписчики регистрируются в начале игры и не изменяются, выделение памяти минимально

Если же это всё равно проблема, можно избежать динамического выделения памяти вообще. Давайте рассмотрим такой вариант.

Связанный список наблюдателей

В коде, который мы видели ранее, Subject хранит список указателей на всех своих наблюдателей. При этом Observer ничего не знает об этом списке и просто реализует интерфейс.

Обычно такой подход предпочтителен, поскольку интерфейсы делают код более гибким.

Но если мы готовы немного изменить Observer, можно встроить управление списком наблюдателей прямо в них. Вместо того чтобы субъект хранил отдельную коллекцию указателей, наблюдатели сами будут звеньями связанного списка:

public class Observer { public Observer NextObserver { get; set; } // Ссылка на следующий узел списка public virtual void Update() { } }

Теперь Subject не хранит массив подписчиков, а просто держит ссылку на первый элемент списка. Это полностью устраняет динамическое выделение памяти при подписке/отписке.

Таким образом, Observer становится частью списка, а сам Subject просто перебирает его. Это позволяет сохранить гибкость паттерна и при этом избежать ненужных аллокаций.

Шаблоны программирования в разработке игр: «Наблюдатель» (лонг)

Чтобы реализовать этот подход, сначала удалим массив наблюдателей в Subject и заменим его указателем на первый элемент списка наблюдателей:

class Subject { Subject() : head_(NULL) {} // Methods... private: Observer* head_; };

Затем мы расширим Observer, добавив указатель на следующего наблюдателя в списке.

class Observer { friend class Subject; public: Observer() : next_(NULL) {} // Other stuff... private: Observer* next_; };

Мы также делаем Subject дружественным классом. Субъект владеет API для добавления и удаления наблюдателей, но теперь сам список находится внутри класса Observer.

Самый простой способ дать Subject доступ к управлению этим списком — сделать его дружественным (friend class).

Регистрация нового наблюдателя сводится к его добавлению в начало списка. Мы выберем самый простой вариант — вставлять новый элемент в начало списка:

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_; } }

Поскольку у нас односвязный список, нам нужно пройти по нему, чтобы найти наблюдателя, которого мы хотим удалить.

Впрочем, то же самое пришлось бы делать и при использовании обычного массива.

Если бы мы использовали двусвязный список, где каждый наблюдатель хранит указатели как на следующего, так и на предыдущего наблюдателя, то удаление наблюдателя можно было бы выполнить за постоянное время (O(1)).

Если бы это был реальный продакшн-код, я бы именно так и сделал.

Остаётся только реализовать отправку уведомлений, а это просто обход списка:

void Subject::notify(const Entity& entity, Event event) { Observer* observer = head_; while (observer != NULL) { observer->onNotify(entity, event); observer = observer->next_; } }

Не так уж и плохо, правда? Субъект может иметь столько наблюдателей, сколько ему нужно, без единого намёка на динамическое выделение памяти.

Регистрация и удаление наблюдателей работают так же быстро, как и с простым массивом.

Но мы пожертвовали одной маленькой возможностью.

Так как мы используем сам объект наблюдателя в качестве узла списка, это означает, что он может принадлежать только к одному списку наблюдателей одновременно.

Другими словами:

  • Один наблюдатель может следить только за одним субъектом.
  • В традиционной реализации, где каждый субъект хранит свой независимый список, наблюдатель мог бы быть подписан сразу на несколько субъектов.

Возможно, вас устроит такое ограничение.В большинстве случаев у одного субъекта бывает несколько наблюдателей, а не наоборот.

Но если вам нужно, чтобы наблюдатель мог подписываться сразу на несколько субъектов, есть более сложное решение, которое по-прежнему не требует динамической памяти.

Оно слишком громоздкое для этой главы, но я опишу его, а вы сможете додумать детали…

Пул узлов списка

Как и раньше, каждый субъект будет иметь связанный список наблюдателей.

Однако теперь узлы списка не будут самими объектами-наблюдателями.

Вместо этого мы создадим отдельные маленькие "узлы списка", которые будут содержать:

  • Указатель на наблюдателя
  • Указатель на следующий узел в списке

Таким образом, наблюдатель сможет находиться в нескольких списках одновременно, а мы избежим динамического выделения памяти.

Шаблоны программирования в разработке игр: «Наблюдатель» (лонг)

Поскольку несколько узлов могут указывать на одного и того же наблюдателя, это означает, что наблюдатель может одновременно находиться в списках нескольких субъектов. Мы снова можем наблюдать за несколькими субъектами одновременно.

Связанные списки бывают двух видов. В том, который вы изучали в школе, есть объект-узел, содержащий данные. В нашем предыдущем примере со связанными наблюдателями всё было наоборот: данные (в данном случае наблюдатель) содержали узел (то есть указатель next_).

Второй стиль называется «интрузивным» связанным списком, потому что использование объекта в списке вторгается в определение самого объекта. Это делает интрузивные списки менее гибкими, но, как мы видели, более эффективными. Они популярны, например, в ядре Linux, где такой компромисс оправдан.

Способ избежать динамического выделения памяти прост: поскольку все эти узлы имеют одинаковый размер и тип, их можно предварительно выделить в виде пула объектов. Это даёт фиксированный набор узлов списка, которые можно использовать и переиспользовать без обращения к реальному распределителю памяти.

Оставшиеся проблемы

Я думаю, что нам удалось избавиться от трёх основных страхов, которыми отпугивают людей от этого паттерна. Как мы увидели, он простой, быстрый и может хорошо работать с управлением памятью. Но означает ли это, что наблюдатели — это универсальное решение?

Это уже другой вопрос. Как и все паттерны проектирования, Observer не является панацеей. Даже при правильной и эффективной реализации он может оказаться не лучшим решением.

Причина, по которой паттерны проектирования получают плохую репутацию, заключается в том, что люди применяют хорошие паттерны к неподходящим задачам и в итоге только усложняют код.

Остаются две проблемы — одна техническая, а другая связана с поддерживаемостью кода. Начнём с технической, потому что их всегда проще решать.

Уничтожение субъектов и наблюдателей

Пример кода, который мы разобрали, надёжен, но он обходит стороной одну важную проблему: что происходит, когда удаляется субъект или наблюдатель?

Если неосторожно вызвать delete для какого-то наблюдателя, субъект всё ещё может хранить указатель на него. Этот указатель теперь висит в памяти, указывая на уже освобождённое место. Когда субъект попытается отправить уведомление… ну, скажем так, приятного в этом будет мало.

Не хочу никого обвинять, но замечу, что в книге “Design Patterns” этот вопрос вообще не рассматривается.

Удаление субъекта происходит проще, так как наблюдатели обычно не хранят на него ссылки. Но даже в этом случае отправка субъекта в утиль памяти может вызвать проблемы.

Наблюдатели могут по-прежнему ожидать получать уведомления, и они не знают, что субъект уже уничтожен. На самом деле, они больше не являются наблюдателями, они просто думают, что ими остаются.

Как с этим справиться?

Есть несколько способов решить эту проблему.

Самый простой вариант — просто игнорировать её, что я и сделал.

В этом случае ответственность за отписку лежит на самом наблюдателе. Он должен сам удалять себя из списков наблюдаемых субъектов перед уничтожением.

Чаще всего наблюдатель знает, за какими субъектами он следит, так что достаточно просто добавить вызов removeObserver() в его деструктор.

Как это часто бывает, самая сложная часть — не реализация, а необходимость помнить, что это нужно сделать.

Если вы не хотите оставлять наблюдателей "висеть" после удаления субъекта, это легко исправить.

Достаточно сделать так, чтобы субъект перед удалением отправлял "последний вздох" в виде уведомления.

Тогда любой наблюдатель сможет обработать это событие и предпринять необходимые действия.

Оплакать, отправить цветы, сочинить элегию и так далее.

Люди — даже те из нас, кто проводит достаточно времени среди машин, чтобы перенять их точность, — ужасно ненадёжны в вопросах соблюдения правил.

Именно поэтому мы и изобрели компьютеры: они не совершают тех ошибок, которые совершаем мы.

Более надёжное решение — сделать так, чтобы наблюдатели автоматически отписывались от всех субъектов при уничтожении.

Если реализовать этот механизм один раз в базовом классе наблюдателя, всем остальным программистам не придётся об этом помнить.

Но это добавляет сложности.

Теперь каждый наблюдатель должен хранить список субъектов, за которыми он следит.

В итоге мы получаем двусторонние указатели.

Не переживайте, у меня есть сборщик мусора

Все вы, продвинутые программисты, использующие модные современные языки со сборщиком мусора, сейчас, наверное, чувствуете себя вполне уверенно.

Думаете, что вам не нужно беспокоиться об этом, потому что вы никогда явно ничего не удаляете?

Подумайте ещё раз!

Представьте: у вас есть экран интерфейса, который отображает характеристики персонажа, например здоровье.

Когда игрок открывает экран, создаётся новый объект.Когда он закрывает его, объект просто забывается, а сборщик мусора должен его убрать.

Каждый раз, когда персонаж получает удар, он отправляет уведомление.Экран UI подписан на эти уведомления и обновляет полоску здоровья. Отлично.

А что произойдёт, если игрок закроет экран, но наблюдатель не отписался?

Экран больше не отображается, но он не будет собран сборщиком мусора, потому что список наблюдателей у персонажа всё ещё содержит ссылку на него.

Каждый раз, когда игрок заново открывает экран, в список добавляется ещё один экземпляр.

На протяжении всей игры, пока персонаж бегает, дерётся и получает урон, он продолжает отправлять уведомления.

Они приходят всем когда-либо созданным экранам, даже если они уже не отображаются.

  • Это тратит процессорное время, обновляя невидимые элементы UI.
  • Если экран проигрывает звуки, их поведение может стать некорректным.

Проблема "утекших слушателей"

Этот баг настолько распространён в системах уведомлений, что у него есть собственное название:Lapsed Listener Problem (Проблема "утекших слушателей").

Так как субъекты хранят ссылки на своих слушателей, можно легко получить "зомби"-объекты, остающиеся в памяти.

Вывод здесь простой:нужно дисциплинированно отписывать наблюдателей, когда они больше не нужны.

Что происходит?

Другая, более глубокая проблема паттерна Observer является прямым следствием его основной цели.

Мы используем его, потому что он ослабляет связь между частями кода.Он позволяет субъекту косвенно взаимодействовать с наблюдателями, не будучи жёстко связанным с ними.

Это действительно удобно, когда вам нужно сосредоточиться на поведении субъекта.Любые посторонние зависимости могли бы отвлекать и мешать.

Если вы работаете с физическим движком, вам вряд ли хочется, чтобы редактор кода (или ваш мозг) засорялся подробностями о системе достижений.

Но если в программе возникает баг, который затрагивает цепочку наблюдателей, то разобраться в потоке уведомлений становится намного сложнее.

Когда связь явно прописана в коде, её легко проследить —достаточно просто посмотреть, какой метод вызывается.

Для обычной IDE это детская задача, потому что связь статическая.

Но если вызовы происходят через список наблюдателей,единственный способ узнать, кто получит уведомление — это смотреть на содержимое списка во время выполнения.

Вместо того чтобы анализировать код статически,вам теперь приходится разбираться в его динамическом поведении.

Как с этим справляться?

Моё правило здесь довольно простое:

👉 Если вам часто приходится думать о двух сторонах коммуникации сразу, не используйте Observer.Вместо этого лучше выразить связь более явно.

Когда вы работаете над большой программой,обычно есть группы связанных компонентов, с которыми приходится разбираться вместе.

Для этого существует множество терминов:

  • Разделение ответственности
  • Связанность и сплочённость (coherence & cohesion)
  • Модульность

Но всё сводится к простому правилу:"Эти вещи должны быть вместе, а эти — отдельно".

Когда Observer полезен, а когда нет?

Паттерн Observer отлично подходит для организации связи между слабо связанными частями программы.Он позволяет им взаимодействовать, не сливаясь в один монолит.

Но он менее полезен внутри одного модуля,если этот код относится к одной функциональности и требует тесного взаимодействия.

Вот почему Observer идеально подходит для нашего примера:

  • Достижения и физический движок — почти не связанные вещи.
  • Скорее всего, ими занимаются разные разработчики.
  • Нам нужен минимальный уровень коммуникации,чтобы работа над одной системой не требовала знаний о другой.

Наблюдатели сегодня

Книга Design Patterns вышла в 1994 году.В то время объектно-ориентированное программирование (ООП) было главным трендом.

Каждый программист на планете хотел «Выучить ООП за 30 дней»,а менеджеры оценивали их работу по количеству созданных классов.

Инженеры мерялись мастерством глубиной своих иерархий наследования.

В тот же год группа Ace of Base выпустила три хита подряд,так что это может кое-что сказать о наших вкусах и суждениях в то время.

Паттерн Observer стал популярным в ту эпоху,поэтому неудивительно, что он основан на классах.

Но сегодня основное сообщество программистов больше ориентировано на функциональный стиль.

Реализовывать целый интерфейс ради одного уведомления —это уже не соответствует современной эстетике кода.

Он кажется громоздким и жёстким. Потому что он таким и является.

Например, нельзя сделать один класс,который использует разные методы для уведомлений от разных субъектов.

Поэтому субъект обычно передаёт себя в вызов onNotify().Так как у наблюдателя только один метод onNotify(),если он подписан на несколько субъектов, ему нужно различать, кто его вызвал.

Современный подход

Сегодня более удобным считается такой вариант,где наблюдатель — это просто ссылка на метод или функцию.

В языках с функциями первого класса и замыканиямиэто намного более распространённый способ реализации наблюдателей.

Сейчас почти каждый язык поддерживает замыкания.

Даже C++ решил эту проблему, несмотря на отсутствие сборщика мусора,а Java наконец-то справилась с этим в JDK 8.

Например:

  • В C# механизм "events" встроен в сам язык.Наблюдателем является делегат — то есть ссылка на метод.
  • В JavaScript система событий позволяет наблюдателям бытьобъектами, реализующими EventListenerили просто функциями (и именно функции используют почти всегда).

Если бы я разрабатывал систему наблюдателей сегодня,я бы сделал её основанной на функциях, а не на классах.

Даже в C++ я бы предпочёл систему,где можно регистрировать указатели на методы,а не создавать экземпляры интерфейса Observer.

Перевод статьи

Еще больше полезной информации по геймдеву в нашем канале по разработке игр:

7
1
1
2 комментария