Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Всем привет ещё раз, пришло время продолжить обсуждение применения 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);
}
}
Думаю, описывать перевод на ссылочный тип контекста других частей проекта не имеет смысла, там всё происходит аналогично тому, как я рассказал выше. Создаём интерфейсы на основе структурных контекстов и подменяем в конструкторах классов.
Скорее всего статья вызовет споры и противоречия, но решил поделиться опытом именно такого рефакторинга, т.к. проделанная работа оправдала затраты и работать стало ощутимо проще.