Почему "не обращайся к родителям" - это вредная догма
Очень часто слышу о том, что обращаться к родительским элементам из дочерних - это антипаттерн.
Я считаю, что возведение этой догмы в абсолют не только не несёт пользы, но и вредит. Ниже я распишу, почему, и как решаются "проблемы", которые влечёт за собой несоблюдение этой догмы.
Почему это считается антипаттерном?
Аргументами против такого подхода, как правило, выступают:
- Невозможность тестить ноду отдельно от родительских;
- При каких-либо перестановках в сценах всё сломается;
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()
.
Однако, взамен обращению к родителям создаются специальные компоненты-контексты, которые содержат данные и логику.
Такой подход позволяет инкапсулировать логику. Т.е. в данном случае нам не нужно создавать руками стейт для дропдауна, дописывать каждой кнопке необходимость его изменять итд..
Хотя по факту это ничем не отличается от "обращения к родителям".
Кроме отсутствия пути до этих родителей (от чего мы, в общем-то, избавились несколькими абзацами ранее).