Шаблоны Проектирования и Unity3D — Часть первая (Агрегатор Событий)

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

Причин подобного явления может быть много, у меня даже есть несколько теорий (не Илюминаты — клянусь!), но пост не об этом. Вместо того чтобы бурчать себе под нос о том как же всё плохо, я решил написать серию постов о том, как по моему мнению стоит и как не стоит делать некоторые вещи. И начну я пожалуй с Паттернов Проектирования (Шаблонов Проектирования).

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

Итак… С чего то нужно начинать, вот и начну я с Агрегатора Событий. Что же это такое? Тем кто знает Английский я рекомендую почитать вот эту статью. Для остальных приведу перевод описания:

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

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

Перед тем как перейти к деталям, я бы хотел рассказать с какими ситуациями и вариантами я встречался, а так же почему не стоит пользоваться этими приёмами и методами. В данной статье я предоставлю задачу (и её решение), с минимальным количеством «участников», однако рекомендую вам представлять что это часть большого проекта, где увеличение объектов будет приводить к всё более плачевным результатам. Как именно? — Сейчас расскажу.

Задача:

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


1. Статические переменные

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

Вот что тут происходит: Каждая Клетка проверяет нажал ли на неё пользователь. Если нажал, Клетка обращается к инстансам Поля и Текста чтобы запустить выполнение функций.

Что же тут плохого? Вроде бы всё чисто, красиво, синглтоны опять же…

  1. Нет, не чисто и не красиво. При увеличении кода и объектов зависящих друг от друга, код будет превращаться в одну сплошную макаронину. Большое количество лишних обращений к сторонним объектам сделает код как минимум, менее читаемым, а как максимум совершенно отвратительным.
  2. Клетка, будучи отдельным компонентом совершенно не должна знать о том что в нашей сцене присутствует Текст или что она является частью поля. По «мнению» Клетки, она вообще должна быть одна. При добавлении (разработке) каждого нового объекта, которому нужно будет знать когда пользователь нажал на кнопку, вам придётся изменять код клетки, что опять же — в итоге превратит ваш код в одну большую кашу.
  3. Утечка памяти. О да, вы не ослышались! Стоит вам «забыть» обнулить вашу переменную «Instance» (а это непременно случится — это всегда случается) и вот ваш объект всё ещё сидит в памяти когда он вам давно не нужен, где никакой Garbage Collector ему не угроза. И «призраки» будут множится параллельно объёму вашего проекта и жрать, жрать, жрать ресурсы системы. Вы пишете на мобильные устройства? — Прелестно. ПРЕЛЕСТНО!

2. Ссылки на объекты

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

Вот что тут происходит: В отличии от метода со статическими переменными, здесь мы храним ссылки на объекты внутри класса который собирается обращаться к этим объектам. Читаемость данного кода улучшит поиск нужных нам объектов в методе «Start», но для данного примера предположим что эти переменные заселены через инспектор Unity.

Что, и тут что то не так? — Да, не так:

  1. У нас опять встаёт гастрономический вопрос — будут макароны или нет? Конечно будут! По сути, в плане макаронного кода, этот метод не отличается от предыдущего, за исключением того, что переменные содержащие объекты объявлены в другом месте.
  2. И снова наша бедная клетка должна считаться с другими объектами. Ну почему она не может просто жить, сама по себе? Почему она должна знать кто ещё находится на данной сцене?
  3. Утечка памяти. Да, да, опять. Только на этот раз шансы намного выше. Мы тут имеем двустороннюю ссылку (Two-way reference). Класс Поле держит список Клеток, тогда как каждая клетка держит ссылку на Поле. Подобное допускать нельзя! Дело в том, что Garbage Collector подбирает объекты только тогда, когда они уничтожены и никто больше не ссылается на них. Когда два объекта ссылаются друг на друга, это приводит к тому, что объекты не уничтожаются, соответственно они пируют. А чем они пируют? — Макаронами Оперативной памятью. Я что то писал о мобильных платформах?

3. События

Данный метод, по какой то непонятной причине, менее распространён среди программистов Unity. Порой мне кажется что они либо не знают, либо ненавидят event’ы.

Здесь мы добавили делегат, который собственно и послужит нам типом события. Теперь, когда на клетку нажмут, многострадальная (в предыдущих вариациях) клетка, наконец то сможет просто «крикнуть» в пустоту — «НА МЕНЯ НАЖАЛИ» и продолжить жить дальше, не считаясь ни с кем и не зная о том, получил кто то её сообщение или нет. Её это не интересует — она сама по себе. Сильная и независимая же клетка!

Ну наконец то что то стоящее, скажете вы. В принципе — да. Но мы пока не дотягиваем…

  1. Слишком много подписок на события, которые должен поддерживать один объект. Если обратите внимание, класс Field подписывается на события от каждой клетки. Представим что наше поле — 50*50. Такое, небольшое, но хорошее поле. 2,500 клеток. Это 2,500 элементов, на которые наше поле подписано. Это 2,500 шансов что, что то пойдёт не так. 2,500 ненужных связей. Мы просто делаем из нашего поля, извините, проститутку! А кроме шуток, всё та же опасность, что наша оперативка будет скушана или появится какая нибудь ошибка, которую будет просто не реально отловить в таком количестве событий.
  2. Передача события. В нашем случае, когда Поле получает извещение о том что клетка нажата, оно запускает своё событие, чтобы Текст мог обновиться. Рассказать альтернативу? Ещё 2,500 регистраций на событие в скрипте Текста и содержание ссылки на все клетки или же на само поле, которое сделает клетки Публичными. Передача события, в данном случае — это меньшее из зол. Это просто делает код менее читаемым и более сложным для отладки.

4. Агрегатор Событий

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

Как вы могли заметить, мы добавили класс EventsManager. Этот класс и послужит нам Агрегатором. У нас всё так же есть делегат, работающий как event, но на этот раз событие находится в Агрегаторе.

Клетка, Поле и Текст — они все держат ссылку на Агрегатор, кто то для получения извещений, а кто то для отправки сообщений. При нажатии на Клетку, она запускает метод OnTileSelected в Агрегаторе, который в свою очередь запускает событие. И все объекты подписанные на это событие получают извещение.

Чем же это лучше?

  1. Ни один класс не знает о существовании другого — каждый живёт своей жизнью, что логически правильно и существенно увеличивает читаемость кода.
  2. Единственный класс на который продолжают держать ссылку — это сам Агрегатор, соответственно риск утечки памяти снижается до уровня криворукости программиста.
  3. При изменении любого из классов подписчиков, остальные классы не затронуты.
  4. Нет бесконечных подписок на события.
  5. Возможность изменить работу Агрегатора, без изменения в других местах кода. Например реализовать мультитредовую систему отправки сообщений.
  6. Код намного легче воспринимается и отлаживается.

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

Поделитесь с друзьями:
  • 1
    Поделиться

Опубликовано migs

1 комментарий

«Дело в том, что Garbage Collector подбирает объекты только тогда, когда они уничтожены и никто больше не ссылается на них. Когда два объекта ссылаются друг на друга, это приводит к тому, что объекты не уничтожаются, соответственно они пируют. » — современный сборщик мусора удаляет объекты с перекрестными ссылками, проверяя, связаны ли они с корневыми объектами.

Добавить комментарий