Godot
September 17, 2023

Почему "не обращайся к родителям" - это вредная догма

Нет, не к этим родителям

Очень часто слышу о том, что обращаться к родительским элементам из дочерних - это антипаттерн.

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

Почему это считается антипаттерном?

Аргументами против такого подхода, как правило, выступают:

  • Невозможность тестить ноду отдельно от родительских;
  • При каких-либо перестановках в сценах всё сломается;
  • ready в нодах вызывается снизу-вверх. Т.е. сначала ready дёргают дочерние элементы, а затем родительские. Из-за чего обращение к родителям может быть преждевременным, и что-то сломается;

Почему это не всегда анти-патерн

Про зависимость от родителей

Эта история актуальна для абстрактных штук, лежащих в других абстрактных штуках.

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

Однако, далеко не всегда это имеет смысл.

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

Абстрагирование скиллов от юнита-владельца усложняет код, при этом не давая никаких преимуществ.

Там, где ты мог бы напрямую дёрнуть unit.statuses.apply(SomeStatus), вместо этого предлагается создавать абстрактные сигналы, вызывать сначала их, а в юните подписываться на эти сигналы и обрабатывать уже оттуда.

В итоге, следуя этой догме, у тебя будет с пару десятков сигналов со всратыми именами вроде hp_change_requested, status_effect_requested , animation_triggered итд. на каждый частный случай.

Касательно путей

Действительно, если обращаться к родительским элементам через get_parent() или get_node("../../Unit"), при каких-либо перестановках в сцене юнита все скиллы сломаются

Однако, необязательно обращаться именно так.

В своём проекте я написал вот такую утилити-функцию:

Она проходит вверх по дереву родителей до тех пор, пока не найдёт родителя соответствующего класса.

Таким образом, в скиллах можно просто писать:

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

Тут кто-то скажет "но как же перфоманс, ты в каждом скилле ходишь на 5 нод вверх".

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

Т.е. в данном примере @onready и никаких вообще проблем.

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

Касательно ready

Действительно, иногда имеется необходимость дёрнуть что-то в родителе, а он ещё не готов. Что же делать?

Очень просто:

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

Аналогия с фронтендом

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

И во многих библиотеках обращение к родительским элементам исключено by-design. Там буквально нет get_parent().

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

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

Хотя по факту это ничем не отличается от "обращения к родителям".

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