Паттерны проектирования в Unity

В предыдущих статьях я упоминал о Unity way и о паттерно-специфичности Unity.

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

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

Вот здесь —

https://refactoring.guru/ru/design-patterns

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

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

Многие юнитисты многое из того о чём рассказано по ссылкам, знают. Однако, всегда находятся индивиды, обвиняющие Unity в кривизне, и неприменимости, в следствие оной, к нему паттернов проектирования софта.

Аргументы этих спорщиков можно разделить на две группы:

  • Unity API сделано контрархитектурно и потому кошеrные паттерны на неё не натянешь.
  • Unity контрархитектурно само по себе, и потому даже если ты на неё натянешь паттерны, результат правильно работать не будет.

Осмысливая эти комменты, я хочу сказать, что, с моей точки зрения, многие авторы комментов мыслят о Unity с позиций классического программирования. И с этих позиций Unity действительно выглядит неправильным. Фактически, программирование под Unity не предусматривает классического ООП, здесь мы манипулируем объектами в среде. Правильнее воспринимать это как написание кода для контроллеров дронов и промышленных роботов. Unity — это фактически, утилита именно для такого рода задач, единственное отличие заключается лишь в том, что здесь манипуляция объектами происходит не в реальном мире а в виртуальном окружении.

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

Что такое паттерны?

По определению, паттерн проектирования — это типичный способ решения часто встречающейся задачи.

Что мы можем почерпнуть из этого определения?

  1. Паттерн — это «типичный способ«. Т.е., наработанный, проверенный поколениями программистов и тысячами имплементаций.
  2. Паттерн — это «способ решения«. То есть это готовый ответ для вас в тех случаях когда вы не знаете, как решать задачу.
  3. Паттерн — это «способ решения часто встречающейся задачи«. Т.е., это готовый ответ, позволяющий вам разгрузить голову, и надёжно решить задачу, уже решённую до вас другими.

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

Особенности народного применения паттернов в Unity.

Нельзя сказать что паттерны проектирования не применяют. Применяют, и даже понимают их важность, и докапываются с ними на собесах… И даже можно расценивать их как секреты ремесла, передающиеся в гильдии от мастеров к неофитам…

Но, с другой стороны… Знаете такую историю про обезьян — сидят обезьяны в клетке, экспериментаторы подвесили им под потолком бананы, дали палки, мол, сбивайте. Обезьяны кидают палки, стараются, потеют. В это время приходит в клетку новая обезьяна, тащит стремянку, и говорит — а давайте сразу залезем и бананы все снимем! В ответ остальные обезьяны начинают дружно п[beep]здить её палками.

Вопрос: «За що???!!!»

Ответ: «А у нас так не принято!»

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

Почему так?

Причин этой безблагодатности две. Первая — в непонимании фактического смысла паттернов программирования. Вторая — в непонимании принципов функционирования Unity.

Фактический смысл паттернов проектирования.

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

Не более и не менее.

Т.е., это не правила, не догмы и не готовые «чертежи». This is more like guidelines than the actual rules.

More like guidelines than the actual rules!

Не более чем рекомендации, весьма усреднённые и теоретические. Причём рассчитанные на весьма абстрактное ООП, не привязанное к особенностям какого-то конкретного технологического стека.

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

Принципы функционирования Unity.

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

  1. Ваш код исполняется в изолированной среде.
  2. Код исполняется дискретно во времени.
  3. Среда исполнения имитирует мир с находящимися в нём объектами.
  4. Ваш код описывает поведения объектов в мире.

Эти 4 положения, с моей точки зрения, наиболее важны с точки зрения понимания методик применения паттернов в Unity.

Положения №1 и №4 фактически, блокируют прямое применение паттернов проектирования в Unity. Unity значительно менее абстрактно чем большинство других технологических стеков (я уже упоминал что это ближе к программированию дронов и роботов), поэтому напрямую использовать привычные паттерны проектирования для решения задач не получится в принципе.

Положение №2 означает, что ваш код исполняется в длинном повторяющемся цикле среди множества другого кода. У вас нет точки входа — ваш код не всегда исполняется под вашим контролем, а так же зависит от очень многих внешних зависимостей внутри среды. Это сильно осложнит применение многих паттернов, т.к., основаны они на разрушении зависимостей ради упрощения кода. В Unity так не получится, и придётся уничтожать зависимости, или, правильнее сказать, делать их несущественными другими способами.

Положения №3 и №4 указывают, что тот код, что вам придётся писать, фактически не будет являться ООП в классическом понимании этого слова. В Unity вам требуется в большей степени алгоритмизация и навыки манипуляции объектами во времени и трёхмерном пространстве, нежели описательные возможности ООП (я имею в виду концепцию абстракций, описания объектов и их свойств).

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

Правильное применение паттернов в Unity.

Так применять или не применять паттерны в Unity-разработке?Обязательно применять! После прочтения предыдущих абзацев вам может показаться что паттерны проектирования в Unity бесполезная и ненужная вещь, но на практике это не так.

Паттерны полезны, по причинам, упомянутым мной в разделе про определение паттернов. Их ключевая задача — разгружать ваши мозги для решения реально нестандартных задач и делать логику вашего кода понятной и стандартной, доступной тем кто придёт поддерживать его после вас. Ваша ключевая задача при этом — понимать КАК ИМЕННО применять паттерны.

Рекомендация №1. «Каждому своё».

Применяя те или иные паттерны в Unity, не пытайтесь ими замещать логику работы собственно Unity. Пример — попытка создания декоратора для имплементации различных компонентов поведений на базе одного исходного интерфейса. Создавая такую штуку, вы обеспечиваете себе развлечение наподобие дрочения в присядку — как в том анекдоте, и на[beep]бёшься, и напляшешься.

Логика Unity — сделать много маленьких MonoBehaviour, описывающих поведение, и развешивать их на префабы, которые затем уже добавлять на сцену. Дайте Unity работать за вас, а сами займитесь более полезным и интеллектуальным делом!

Рекомендация №2. «It’s magic!».

Паттерны являются в том числе методами разрушения зависимостей, или, по крайней мере, приведения их к такому виду который относительно легко воспринимается тренированным человеком. Однако многие паттерны (ну тот же синглтон) в Unity, в силу его архитектуры (environment и много объектов в нём) становятся антипаттернами, и вместо того чтобы дробить зависимости, порождают их. Избегайте этого. Объект должен быть независимым объектом, в том же смысле в котором реальный объект относительно независимо существует в реальном мире. Создавая «невидимые» связи между подвешенными на разные gameObject-ы скриптами вы, фактически, создаёте то что в реальном мире назвали бы магией. Не надо думать что другие программисты тоже волшебники и будут искать все эти связи.

Рекомендация №3. «Скажи словами».

В предыдущих пунктах я уже сказал, что механики порождающих и структурных паттернов в Unity дают сбои. Решить многие проблемы, вызываемые этим, можно с помощью создания механизма передачи информации между объектами на сцене. Информационная шина, организованная на основе подписки на события зарегистрированные в статическом классе, сильно упрощает логическую структуру всей системы объектов на сцене. Мы и в реальной жизни так делаем — вместо того чтобы натягивать дизельпанковые проволочки и верёвочки между приборами в цеху или лаборатории, мы подключаем их к локальной сети, и гоняем между ними нужную нам информацию.

Рекомендация №4. «Квадратный астронавт, круглый люк, корабль около Луны».

Отказывайтесь от механик непосредственного взаимодействия классов в пользу универсальных интерфейсов, унаследованных из самого Unity (как Component, или, например, Collider, без уточнения типа) или обмена информацией через информационную шину или других посредников. Вы можете подумать что здесь я ратую против структурных паттернов типа адаптера. Но на самом деле я просто призываю к их аккуратному и грамотному применению. Когда вы добавляете в ваш самописный компонент публичное поле, в которое нужно будет добавить другой ваш самописный компонент, вы автоматически закладываете в свой код бомбу. Потому что в итоге ваш объект в прямом смысле окажется на сцене где-то «около Луны», и вам придётся писать костыльную систему которая будет находить и подсовывать в эти объекты правильные ссылки на правильные компоненты.

Рекомендация №5. «По деяниям вы узнаете их».

Логика скриптов Unity — описание поведений внутриигровых объектов. Код в Unity описывает в первую очередь именно это: как объект действует. Применение паттернов проектрирования должно концентрироваться именно на оптимизации этого описания действий. Рассматривайте ваш код не как теоретическую структуру из интерфейсов, базовых и наследующих классов. Будьте ближе к практическому применению кода, и структурируйте его не по формальной логике, а по его прикладному значению. Паттерны в Unity должны структурировать код именно по этому прикладному назначению, а не как-либо ещё. Сломайте свой мозг — думайте о коде в Unity-проектах не как об объектах, а именно как о сложной формы функциях. Их фактическое назначение — первично. Для понимания — представьте конечный автомат, реализующий игровой ИИ. Правильный путь его создания — не создавать классы, описывающие состояния объекта, и каким-либо извращённым образом вгружать функции из них в исполняемый цикл state machine. Вместо этого — дробите состояния на компоненты, и простым Component.enabled=true включаете или выключаете их по необходимости, иногда даже пачками.

Подитожим.

Паттерны проектирования в Unity использовать можно и нужно, если это делать правильно. Правильное применение паттернов — это когда при их использовании вы понимаете (можете объяснить словами) зачем вы это делаете, и когда вы используете их в связке с возможностями собственно самого Unity. Для успешного использования паттернов нужно чтобы они не мешали работе систем Unity, дробили зависимости, уменьшали сложность и связность кода, а так же следовали Unity way. Используя их таким образом, вы сможете улучшить свою кодовую базу и облегчить работу как себя, так и своих коллег.