Unity: слабосвязанная архитектура для решения конкретных задач

от автора

Всем привет!

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

Начнем с самой задачи.
Дано: игрок, который перемещается по уровню и собирает монеты.
Цель: отображать текущее количество монет у игрока в интерфейсе игры.

Теория

Реализацию подбора монет, хранения их количества и прочего, опускаем, так как сам может это сделать так, как ему нравится, и сразу перейдем к теории решения этой задачи. Так как элемент интерфейса, который информирует нас о количестве монет один, то логично бы его сделать синглтоном, однако, это вынуждает нас либо везде проверять на null этот объект, либо инстанцировать новый объект если его нет. Постоянные проверки на null — может породить кучу повторяющегося кода, и этого следует избегать. Если мы забудем такую проверку, то получим самую распространенную по статистике ошибку. Инстанциация нового объекта тоже может быть крайне неудобным решением, например, в тестовой сцене, где мы проверяем какие-то механики и нам не нужны элементы интерфейса, или нужны но не все.

Как известно, данную проблему можно решить паттерном «прокси». Если есть такие, кто не знает, то это легко гуглится, но вкратце напомню о нем. «Прокси» — прослойка, которая замещает объект и обеспечивает доступ к нему. Обращение к прокси будет нам гарантировать, что обращение будет произведено к существующему экземпляру объекта. Далее сам прокси определит, существует ли целевой объект или нет. Если существует, то прокси вызовет требуемый метод у целевого объекта.

Раз уж мы хотим таким образом избавиться от дублирования кода и постоянных проверок на null, то логично было бы задуматься, что синглтонов и прокси может быть много, но все они объединены тем, что они синглтоны и прокси. Универсальные классы c# помогут нам решить проблему дублирования кода при их описании.

Решение

Начнем с универсального синглтона.

public class Singleton<T> where T : new()     {         private static T instance;         public static T Instance         {             get             {                 if (instance == null)                     instance = new T();                 return instance;             }         }     }

Наследование от этого класса позволит нам обеспечить создание синглтона. Но он не подойдет для тех классов, которые мы хотим добавить к GameObject, так как они должны наследоваться от MonoBehaviour. А MonoBehaviour, в свою очередь, создается через Instantiate, а не через конструктор. Для него мы можем сделать свой универсальный синглтон.

public class MonoBehaviourSingleton<T> : MonoBehaviour where T : MonoBehaviour {         public static T Instance;          protected virtual void Awake()         {             Instance = GetInstance();         }          protected T GetInstance() {             return this as T;         } }

Теперь наследование от этого класса будет давать нам синглтон для MonoBehaviour объектов.

Чтобы мы могли пользоваться прокси вместо требуемого объекта, нам необходимо создать общий интерфейс и реализовать его в классе CoinUI и его прокси.

 public interface ICoinUI {          void SetCount(int count); } 

Код элемента интерфейса для отображения кол-ва монет может выглядеть так

public class CoinUI : MonoBehaviourSingleton<CoinUI>, ICoinUI {         public Text coinCount;          private void Start()         {          }          public void SetCount(int count)         {                  coinCount.text = count.ToString();         }  }

Тут же создадим прокси-класс для CoinUI, который будет проверять на наличие инстанса CoinUI и, если он существует, вызывать у него требуемый метод.

 public class CoinUIProxy : ICoinUI {         public void SetCount(int count)         {                  CoinUI.Instance?.SetCount(count);         }  }

В случае, если нет инстанса CoinUI, то просто ничего не произойдет.

Перейдем к созданию универсального конструктора прокси.

public class UIProxy<proxyType> : Singleton<proxyType> where proxyType : new() { }

И это весь код. Выше мы уже описали класс-родитель Singleton, так что просто наследуем от него, указывая универсальный тип, в качестве типа синглтона.

Финальным классом для нашей конструкции станет своего рода провайдер — класс, который будет предоставлять нам объект, реализующий интерфейс ICoinUI.

 public class UIProvider  {         public static ICoinUI CoinUI => UIProxy<CoinUIProxy >.Instance; } 

Данный провайдер вернет нам экземпляр класса CoinUIProxy, который при вызове SetCount проверит существует ли CoinUI, если существует, то вызовет у инстанса CoinUI метод SetCount и передаст значение.

Для того, чтобы изменить значение количества монет в интерфейсе игры, Вам потребуется всего одна строка кода

 UIProvider.CoinUI.SetCount(100500); 

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

Выводы

Минусы данного подхода:

  1. Необходимо создать обвязку в виде провайдера и универсального прокси.
  2. Вместо одного класса у нас 2 (основной и прокси) + интерфейс, но в идеале у Вас и так должен быть слабосвязанный код, который подразумевает работу в основном с интерфейсами и их реализациями. так что это минус только для супер ленивых, кто не любит писать код правильно.

Плюсы:

  1. Нет проверок на null во всех местах, где вызывается изменение значения количества монет.
  2. Вызвать метод можно из любого класса и любого места в коде, не переживая за то, что мы забыли добавить объект отображающий кол-во монет на сцену.
  3. Можно спокойно менять внутреннюю структуру CoinUI, чтобы добиться желаемого отображения изменения количества монет — это никак не повлияет на работу основного кода.

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

Надеюсь, статья была полезной. Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/post/426789/


Комментарии

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

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