Ten-Hut. Про архитектуру кода #2

Едем дальше. Зачем это всё, первый подводный камень и его решение простенькой домашней инъекцией зависимостей.

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

Понарассовывали монобехэвиоров везде, какие-то скрипты ищем через SerializeField, какие-то - через GetComponent и FindObjectOfType, значения полей с настройками все разбросаны по куче геймобжектов, вложенных друг в друга и раскиданных по разным сценам и префабам, - в общем, полная неразбериха. И не важно, какой у вас код, SOLID или STUPID, данная-то проблема всё равно остаётся. Если мне нужно писать новый скрипт, мне хочется сразу садиться и его писать, а не думать, в каком виде его делать - базовый C#-класс или монобехэвиор, и если последний, то куда в сцене его класть, не думать, как я буду искать те скрипты, на которые он должен ссылаться, и на каких геймобжектах они вообще лежат.

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

С этим “типа MVC” мне и не надо помнить - он хоть какую-то структуру, но всё-таки даёт. И я знаю, что всё взаимодействие идёт через Главный Синглтон, и что вся логика, которая не обязана быть представлена в виде монобехэвиоров, будет в виде базовых С#-классов, и достучаться до неё можно через всё тот же главный и единственный синглтон, и все настройки достаются через него же, а в редакторе в папке проекта они удобно разложены по скриптабл-обжект-ассетам.

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

Короче, вот.

В общем, начал писать контроллеры. Довольно быстро выяснилось, что поскольку синглтон создаёт экземпляры контроллеров последовательно, один за другим, а в базовых C#-классах вместо двух методов инициализации типа Awake и Start есть только один - конструктор, - получается, что один контроллер может обратиться к другому в тот момент, когда последний ещё не создан, и словить null reference exception.

Временное решение проблемы - упорядочивать создание экземпляров контроллеров в синглтоне нужным образом. Т.е., например, сначала создаём InputController, и только потом - PlayerActionsController, потому что второй ссылается на первый. Однако, во-первых, это банально неудобно - приходится помнить, кто на кого ссылается. А, во-вторых, если мы дойдём до ситуации, когда класс А обращается к классу Б, класс Б обращается к классу В, класс В обращается к классу Г, а класс Г обращается к классам А и Б, тут всё совсем навернётся.

Что делать? Отделить создание экземпляра класса от передачи ссылок на другие объекты. Т.е., грубо говоря, добавить инъекцию зависимостей. Здесь можно не заниматься изобретением велосипеда и просто взять готовый фреймворк (я в прошлом посте уже упоминал ZenJect и Adic), благо это бесплатно. А можно ради интереса попробовать внедрить собственный простенький вариант, что я и сделал.

Для этого пишем интерфейс:

public interface IDependencyInjectionReceiver { void InjectDependencies(); }

В главный синглтон добавляем:

//список контроллеров, которым потребуются ссылки на другие контроллеры private List<IDependencyInjectionReceiver> _dIReceivers = new List<IDependencyInjectionReceiver>(); //метод для подписки на внедрение зависимостей //по сути, здесь мы просто добавляем контроллер в список public void SubscribeToDependencyInjection(IDependencyInjectionReceiver target) { _dIReceivers.Add(target); } //раздаём ссылки всем в списке private void InjectDependencies() { foreach (var target in _dIReceivers) { target.InjectDependencies(); } }

И этот самый InjectDependencies() в главном синглтоне запускаем в тот момент, когда нужно раздать ссылки на объекты. Т.е. ставим его вызов где-нибудь в методе Start(), после того как были созданы все контроллеры.

Далее, в каждом контроллере, который ссылается на любой другой контроллер, добавляем имплементацию интерфейса IDependencyInjectionReceiver, в методе InjectDependencies() подхватываем ссылки на эти самые другие контроллеры из главного синглтона, ну и не забываем в конструкторе подписаться на инъекцию:

public class ControllerA : IDependencyInjectionReceiver { private ControllerB _controllerB; public ControllerA() { //подписываемся на внедрение зависимостей Main.Instance.SubscribeToDependencyInjection(this); } //получаем ссылки public void InjectDependencies() { _controllerB = Main.Instance.ControllerB; } }

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

Монобехэвиоры же тут вообще ни при чём. Поскольку любые монобехэвиоры в DefaultExecutionOrder стоят после главного синглтона, то в них достаточно просто поместить всю передачу ссылок в Start(), к этому моменту и все контроллеры уже есть, и InjectDependencies() уже отработал.

Такая вот простенькая домашняя версия. Если что, существующие фреймворки инъекции зависимостей значительно сложнее, гибче, предоставляют более широкий спектр возможностей (а часто - ещё и дополнительные фичи, не имеющие прямого отношения к DI, типа фабрик и т.п.) и работают несколько иначе. В Adic, например, другим скриптам не нужно вообще ни к какому синглтону напрямую обращаться, просто ставится атрибут [Inject], и все объекты под ним сами подхватывают нужные ссылки. Другое дело, что создание этих ссылок в главном синглтоне там будет выглядеть чуть сложнее.

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

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

круть

1
Ответить