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

Вместо предисловия.
Здравствуйте, коллеги! Меня зовут Даниил, я Unity-разработчик уже почти 10 лет. В настоящее время я занимаюсь уже не столько разработкой, сколько анализом чужих Unity-проектов, и эта работа натолкнула меня на идею написать здесь, на Хабре, цикл статей, посвящённый качеству кода в Unity.
В предыдущей статье я упоминал о Unity way и о паттерно-специфичности Unity. Это вызвало споры в комментах. Я ожидал этого — я уже заводил разговоры на эту тему и на других ресурсах, посвящённых Unity, и там это тоже вызывало споры. Многие аргументы, авторы которых обвиняли Unity в кривизне, достаточно весомы. Однако все эти аргументы можно разделить на две группы:
Unity API сделано контрархитектурно и потому кошеrные паттерны на неё не натянешь.
Unity сделано контрархитектурно само по себе, и потому даже если ты на неё натянешь паттерны, результат правильно работать не будет.
Осмысливая эти комменты, я хочу сказать, что, с моей точки зрения, многие авторы комментов мыслят о Unity с позиций классического программирования. И с этих позиций Unity действительно выглядит неправильным. Фактически, программирование под Unity не предусматривает классического ООП, здесь мы манипулируем объектами в среде. Правильнее воспринимать это как написание кода для контроллеров дронов и промышленных роботов. Unity — это фактически, утилита именно для такого рода задач, единственное отличие заключается лишь в том, что здесь манипуляция объектами происходит не в реальном мире, а в виртуальном окружении.
Натягивать на это паттерны классического ООП — неэффективно. С другой стороны, мы хорошо знаем, что паттерны улучшают консистентность кода, способствуют стабильности продукта и упрощают разработку и поддержку. Возникает вопрос — что же предпринять? Использовать паттерны, или не использовать? И если использовать, то как?
Что такое паттерны?
По определению, паттерн проектирования — это типичный способ решения часто встречающейся задачи.
Что мы можем почерпнуть из этого определения?
Во-первых, паттерн — это »типичный способ». Т.е., наработанный, проверенный поколениями программистов и тысячами имплементаций.
Во-вторых, паттерн — это «способ решения». То есть это готовый ответ для вас в тех случаях когда вы не знаете, как решать задачу.
В третьих, паттерн — это «способ решения часто встречающейся задачи». Т.е., это готовый ответ, позволяющий вам разгрузить голову, и надёжно решить задачу, уже решённую до вас другими.
Казалось бы, всё очевидно, применяем и не паримся? И ведь применяют же. Однако почему до сих пор Unity-проекты, попадающие ко мне на анализ, состоят почти полностью из shitcode, а написанные на Unity игры через одну безбожно тормозят и глючат?
Особенности народного применения паттернов в Unity.
Нельзя сказать что паттерны проектирования не применяют. Применяют, и даже понимают их важность, и достают ими на собесах… И даже можно расценивать их как секреты ремесла, передающиеся в гильдии от мастеров к неофитам…
Но, с другой стороны… Знаете такую историю про обезьян — сидят обезьяны в клетке, экспериментаторы подвесили им под потолком бананы, дали палки, мол, сбивайте. Обезьяны кидают палки, стараются, потеют. В это время приходит в клетку новая обезьяна, тащит стремянку, и говорит -, а давайте сразу залезем и бананы все снимем! В ответ остальные обезьяны начинают дружно бить её палками.
Вопрос:»За що???!!! »
Ответ:»А у нас так не принято! »
Так вот, иногда применение патернов в Unity очень похоже на поведение вышеописанных обезьян — причём как на поведение тех которые с палками, так и той которая с лестницей.
Почему так?
Причин этой безблагодатности две. Первая — в непонимании фактического смысла паттернов программирования. Вторая — в непонимании принципов функционирования Unity.
Фактический смысл паттернов программирования.
Что касается первой причины, то действительно, очень многие понимают смысл паттернов программирования слишком серьёзно и буквально. Хотя, если отбросить всю пафосную шелуху про стабильность и консистентность кода, паттерны программирования — это просто набор рекомендаций о том, как решать шаблонные задачи программирования.
Не более и не менее.
Т.е., это не правила, не догмы и не готовые «чертежи». This is more like guidelines than the actual rules.
Не более чем рекомендации, весьма усреднённые и теоретические. Причём рассчитанные на весьма абстрактное ООП, не привязанное к особенностям какого-то конкретного технологического стека.
К сожалению, в индустрии полно программистов, воспринимающих шаблоны проектирования как догмы, коим следует следовать и кои следует применять без внятного понимания -, а зачем оно, собственно, нужно.
Принципы функционирования Unity.
Говорить о второй причине несколько сложнее, потому что не очень понятно, какие принципы функционирования Unity мы должны держать в голове для понимания применимости паттернов. Исходя из моего опыта, надо выделять следующие ключевые принципы:
Ваш код исполняется в изолированной среде.
Код исполняется дискретно во времени.
Среда исполнения имитирует мир с находящимися в нём объектами.
Ваш код описывает поведения объектов в мире.
Эти 4 положения, с моей точки зрения, наиболее важны с точки зрения понимания методик применения паттернов в Unity.
Положения №1 и №4 фактически, блокируют прямое применение паттернов программирования в Unity. Unity значительно менее абстрактно чем большинство других технологических стеков (я уже упоминал что это ближе к программированию дронов и роботов), поэтому напрямую использовать привычные паттерны проектирования для решения задач не получится в принципе.
Положение №2 означает, что ваш код исполняется в длинном повторяющемся цикле среди множества другого кода. У вас нет точки входа — ваш код не всегда исполняется под вашим контролем, а так же зависит от очень многих внешних зависимостей внутри среды. Это сильно осложнит применение многих паттернов, т.к., основаны они на разрушении зависимостей ради упрощения кода. В Unity так не получится, и придётся уничтожать зависимости, или, правильнее сказать, делать их несущественными другими способами.
Положения №3 и №4 указывают, что тот код, что вам придётся писать, фактически не будет являться ООП в классическом понимании этого слова. В Unity вам требуется в большей степени алгоритмизация и навыки манипуляции объектами во времени и трёхмерном пространстве, нежели описательные возможности ООП (я имею в виду концепцию абстракций, описания объектов и их свойств).
Таким образом, классические паттерны программирования при использовании их в Unity как минимум, не всегда работают, а как максимум — становятся антипаттернами.
Правильное применение паттернов в Unity.
Так применять или не применять паттерны в Unity-разработке?
Обязательно применять! После прочтения предыдущих абзацев вам может показаться что паттерны проектирования в Unity бесполезная и ненужная вещь, но на практике это не так.
Паттерны полезны, по причинам, упомянутым мной в разделе про определение паттернов. Их ключевая задача — разгружать ваши мозги для решения реально нестандартных задач и делать логику вашего кода понятной и стандартной, доступной тем кто придёт поддерживать его после вас. Ваша ключевая задача при этом — понимать КАК ИМЕННО применять паттерны.
Рекомендация №1. «Каждому своё».
Применяя те или иные паттерны в Unity, не пытайтесь ими замещать логику работы собственно Unity. Пример — попытка создания декоратора для имплементации различных компонентов поведений на базе одного исходного интерфейса. Создавая такую штуку, вы обеспечиваете себе развлечение наподобие мастурбации вприсядку (ЕВПОЧЯ). Логика 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. Для успешного использования паттернов нужно чтобы они не мешали работе систем Unity, дробили зависимости, уменьшали сложность и связность кода, а так же следовали Unity way. Используя их таким образом, вы сможете улучшить свою кодовую базу и облегчить работу как себя, так и своих коллег.
В последующих статьях я предполагаю, в том числе, более подробно разобрать типичные паттерны, и, что самое главное, антипаттерны, применяющиеся в проектах Unity, а так же рассмотреть другие аспекты создания качественного кода в Unity.
Если у вас есть какие-то свои мысли по этому поводу — прошу в комментарии. Ну, вот так как-то.
