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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру 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. И тут стоит обратить внимание на два момента:

  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/


Интересные статьи

Интересные статьи

Стремясь к профессиональному росту, многие разработчики делают ставку на конкретные технологии, новые языки и другие аспекты, связанные с IT. При этом IT-решение – часть бизнес-процесса, которая делае...
Большая часть информации в мире хранится в виде таблиц, которые можно найти в Интернете или в базах данных и документах. В таблицах может находиться всё что угодно, от технических характеристик потреб...
Данная статья представляет собой обзор на оригинальную статью на Medium (эксперименты проводятся с изменениями некоторых условий =).Область применения нейронных сетей в м...
Поезд null safety мчится вперёд, уже почти официально анонсирован Flutter 2.0 (подключайтесь к предстоящему Flutter Engage), экосистема Dart тоже не стоит на месте. Мы пе...
Готовим соус для запоминания слов. Нам понадобятся:1) Мнемотехника2) Reverso Context3) Тайм-менеджемент Читать далее