Всем привет ещё раз, пришло время продолжить обсуждение применения Composition root в Unity. В прошлой статье я описал основные положения данной архитектуры, способы проброса зависимостей и организацию примитивной логики в рамках проекта. А также, обещал рассмотреть замену контекстных структур на интерфейсы глобального класса. Вот этим и займёмся.

Откуда взялась потребность?
Передавать контекст в виде структур очень экономично с точки зрения памяти, они лежат на стеке и плюсы этого очевидны, но есть и минусы, заключающиеся как раз в практике использования. Когда я только познакомился с этой архитектурой, то подразумевалась подача контекста именно в виде структур, что не так уж и плохо, если в команде не так много программистов. Со временем наша команда росла и новым участникам становилось сложнее разбираться в “ветвях” зависимостей и отслеживать все возможные изменения реактивных переменных. Как минимум, не хватало простейшей функции редактора Find Usage. Так пришло решение отойти от постоянно создаваемы контекстов и использовать глобальный класс, где будут описаны все используемые переменные и события.
Реализация шаг 1.
В рамках того же проекта создаём два скрипта Interfaces и GlobalContext. В первом убираем автоматически созданный класс и перенесём контекст UIEntity в виде интерфейса:
Интерфейс UIEntityCtx
public interface UIEntityCtx { ContentProvider contentProvider { get; set; } RectTransform uiRoot { get; set; } ReactiveProperty<int> buttonClickCounter { get; set; } }
В свою очередь в GlobalContext нужно имплементировать этот интерфейс:
Класс GlobalContext
public class GlobalContext: UIEntityCtx { public ContentProvider contentProvider { get; set; } public RectTransform uiRoot { get; set; } public ReactiveProperty<int> buttonClickCounter { get; set; } }
Реализация шаг 2.
Теперь необходимо создать объект GlobalContext и заполнить переменные. Можно создать его прямо в EntryPoint, но чтобы не наполнять статью кодом я перенесу этот момент в GameEntity. Добавляем переменную и задаём значения в конструкторе:
Класс GameEntity
private readonly Ctx _ctx; private UIEntity _uiEntity; private CubeEntity _cubeEntity; private readonly ReactiveProperty<int> _buttonClickCounter = new ReactiveProperty<int>(); private GlobalContext _globalContext; public GameEntity(Ctx ctx) { _ctx = ctx; _globalContext = new GlobalContext(); _globalContext.contentProvider = _ctx.contentProvider; _globalContext.uiRoot = _ctx.uiRoot; _globalContext.buttonClickCounter = _buttonClickCounter; CreateUIEntity(); CreteCubeEntity(); } private void CreateUIEntity() { _uiEntity = new UIEntity(_globalContext); AddToDisposables(_uiEntity); }
Сейчас UIEntity в качестве параметра принимает уже не структуру Ctx, а интерфейс UIEntityCtx, значения которого были заданы уровнем выше.
Реализация шаг 3.
Создадим таким же образом контекстные интерфейсы для UIPm и UIviewWithButton. И тут стоит обратить внимание на два момента:
-
Интерфейс
UIEntityCtxдолжен содержать в себе интерфейсыUIPmCtxиUIviewWithButtonCtx, таким образом мы получаем иерархию внутри контекстов. Сделано это для того чтобы ограничить “область видимости” каждого компонента и добиться лучшей инкапсуляции. Можно было бы везде отдавать_globalContextв качестве зависимостей и всё бы так же работало, но это уже вопрос безопасности. -
В классе
GlobalContextнеобходимо имплементировать два новых интерфейса —UIPmCtxиUIviewWithButtonCtx, указав что при обращении они ссылаются именно на этот класс, чтобы не получить null. Сделать это можно, к примеру, вот таким образом publicUIPmCtx uIPmCtx {get => this; set { }}
Скрипт Interfaces
public interface UIEntityCtx { ContentProvider contentProvider { get; set; } RectTransform uiRoot { get; set; } ReactiveProperty<int> buttonClickCounter { get; set; } UIPmCtx uIPmCtx { get; set; } UIviewWithButtonCtx uIviewWithButtonCtx { get; set; } } public interface UIPmCtx { ReactiveProperty<int> buttonClickCounter { get; set; } } public interface UIviewWithButtonCtx { ReactiveProperty<int> buttonClickCounter { get; set; } }
Класс GlobalContext
public class GlobalContext: UIEntityCtx, UIPmCtx, UIviewWithButtonCtx { public ContentProvider contentProvider { get; set; } public RectTransform uiRoot { get; set; } public ReactiveProperty<int> buttonClickCounter { get; set; } public UIPmCtx uIPmCtx { get => this; set { }} public UIviewWithButtonCtx uIviewWithButtonCtx { get => this; set { }} }
Для того чтобы значение переменных в контекстах дошли до конечной точки, необходимо, как же как в прошлой версии проекта, заполнить их при создании объектов. Значения передаются сверху вниз от объекта родителя.
Обновлённый класс UIEntity
public class UIEntity : DisposableObject { private readonly UIEntityCtx _ctx; private UIPm _pm; private UIviewWithButton _view; public UIEntity(UIEntityCtx ctx) { _ctx = ctx; CreatePm(); CreateView(); } private void CreatePm() { _ctx.uIPmCtx.buttonClickCounter = _ctx.buttonClickCounter; _pm = new UIPm(_ctx.uIPmCtx); AddToDisposables(_pm); } private void CreateView() { _view = Object.Instantiate(_ctx.contentProvider.uIviewWithButton, _ctx.uiRoot); _ctx.uIviewWithButtonCtx.buttonClickCounter = _ctx.buttonClickCounter; _view.Init(_ctx.uIviewWithButtonCtx); } protected override void OnDispose() { base.OnDispose(); if(_view != null) Object.Destroy(_view.gameObject); } }
Думаю, описывать перевод на ссылочный тип контекста других частей проекта не имеет смысла, там всё происходит аналогично тому, как я рассказал выше. Создаём интерфейсы на основе структурных контекстов и подменяем в конструкторах классов.
Скорее всего статья вызовет споры и противоречия, но решил поделиться опытом именно такого рефакторинга, т.к. проделанная работа оправдала затраты и работать стало ощутимо проще.
ссылка на оригинал статьи https://habr.com/ru/post/711140/
Добавить комментарий