Использование архитектуры Composition root в Unity. Часть 2. Переход на зависимости ссылочного типа

от автора

Всем привет ещё раз, пришло время продолжить обсуждение применения 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. И тут стоит обратить внимание на два момента:

  1. Интерфейс UIEntityCtx должен содержать в себе интерфейсы UIPmCtx и UIviewWithButtonCtx, таким образом мы получаем иерархию внутри контекстов. Сделано это для того чтобы ограничить “область видимости” каждого компонента и добиться лучшей инкапсуляции. Можно было бы везде отдавать _globalContext в качестве зависимостей и всё бы так же работало, но это уже вопрос безопасности.

  2. В классе GlobalContext необходимо имплементировать два новых интерфейса — UIPmCtx и UIviewWithButtonCtx, указав что при обращении они ссылаются именно на этот класс, чтобы не получить null. Сделать это можно, к примеру, вот таким образом public UIPmCtx 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *