Pull to refresh

Comments 17

Я уже запутался в сущностях. Раз уж сервис-локатор всё-равно используется, то MoveController проще выглядит c таким методом.

public void Construct(GameLocator gameLocator)
{
	this.input = gameLocator.GetService<IMoveInput>();
	this.player = gameLocator.GetService<IPlayer>();
}

Так можно избавиться от GameAssembler, который будет только бухнуть со временем (или их будет много разных?).

Да и сервис-локатор можно взять статический https://github.com/Leopotam/globals/blob/master/src/Service.cs , убрав GameLocatorInstaller. Всё-таки сервисы, которые юзаются через сервис-локатор, обычно знают, что они единичные, и регистрировать самим себя - нагляднее при чтении их кода.

Но это всё актуально, только если идти честным путём сервис-локатора, а не как промежуточный этап к самописному DI :)

Привет @Tr0sT

public void Construct(GameLocator gameLocator)
{
this.input = gameLocator.GetService<IMoveInput>();
this.player = gameLocator.GetService<IPlayer>();
}

Лучше так не делать в средних и крупных проектах, так как это анти-паттерн. При таком стиле написания классов, у тебя вся логика в проекте будет зависеть от GameLocator, и ты не сможешь нормально переиспользовать эти классы в других проектах)

От ассемблера пока не рекомендую избавляться, потому что в третьей части (спойлер) мы объединим классы GameObservableInstaller, GameLocatorInstaller и GameAssembler в один монобех-инсталлер, который будет заниматься созданием экземпляров с бизнес-логикой, регистрацией сервисов и лисенеров + прокидыванием в них зависимостей. И получиться так, что для каждой фичи игры будет свой инсталлер, похожий на MonoInstaller в Zenject. Например, для фичи апгрейдов будет UpgradesInstaller, для фичи квестов QuestInstaller и так далее. В результате на сцене останутся монобехи-инсталлеры и игровые объекты.

По поводу статического сервис-локатора — лучше сделать такой центральный реестр на уровне контекста приложения, а не на уровне игровой сцены. Ты можешь честно идти путем сервис-локатора, а потом поверх него написать алгоритм для DI через Reflection

Получается какая-то мешанина из ответственностей. И игрок, и клавиатура, и контроллер движения теперь вообще сервисы. Получается "прокинь что хочешь куда хочешь и будь что будет". А сервис локатор только упрощает такое поведение.
Почему бы тогда не прокинуть контроллер в игрока или ввод в игрока или игрока сразу в ввод? Раз уж они все сервисы.
Весь контроль управления зиждется на том, что мы постараемся не забыть что в проекте главнее)

@redHurt69Привет!

Спасибо за обратную связь)

Игрок, клавиатура и контроллер движения, как были компонентами в системе со своими ответственностями и бизнес-логикой, так и остались. Просто в проекте нам нужно сделать механизм, с помощью которого мы будем получать эти объекты в системе. Но с точки зрения сервис-локатора, они все сервисы, не потому что они делают какую-то "сервисную логику", а потому что просто такая конвенция между разработчиками

Если режет глаз, то можно назвать класс-реестр GameContext и вызывать GameContext.GetInstance<T>()

Их "ответственность" по сути контролируется только за счет того, что мы "считаем" контроллер главным.
Что мешает нам или другому программисту в следующей фиче сделать зависимость игрока на контроллер или клавиатуру? Только очередная какая-то негласная конвенция и ориентацией на волшебные слова в названиях классов - "ну все что называется контроллер это типа важнее"
Данная архитектура всего лишь упрощает доступность данных, а не решает вопрос "как мне правильно написать следующую фичу". А в плане доступности данных юнити на шаг впереди этого сервис локатора. Можно с тем же успехом просто находить зависимости через FindObjectOfType<T>

Вот тут не очень понял проблемы, но звучит прикольно ?:

1.Получается, "прокинь что хочешь куда хочешь и будь что будет"

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

По сути все о том же, что и комментом выше

От ассемблера пока не рекомендую избавляться, потому что в третьей части (спойлер) мы объединим классы GameObservableInstaller, GameLocatorInstaller и GameAssembler в один монобех-инсталлер, который будет заниматься созданием экземпляров с бизнес-логикой, регистрацией сервисов и лисенеров + прокидыванием в них зависимостей. 

Вот тут лично мне кажется, что принцип единственной ответственности будет сильно нарушаться - слишком много причин для изменений получается (если следовать определению единственной ответственности, предложенному господином Мартином), поправьте меня, если я ошибаюсь. Почему нельзя оставить так как есть?

Потому что помимо SOLID есть еще шаблоны GRASP и принцип KISS. Самое главное — система должна быть простая и понятная, поэтому зачастую необходимо уменьшать кол-во классов в системе, перераспределяя и иногда нарушая SRP, чтобы решение было более компактным)

Вы в целом правильно описали проблему, которая приписывается сервис локатору.
Но получается забавно - пытаясь избавиться от DI, вы переизобрели DI
По сути если унифицировать класс assembler - это и будет DI
Например можно сделать его универсальным через рефлексию или рефлексию + кодогенерацию, чтобы не писать много однотипного кода.

Еще комментарии:
- Сервис локатор можно сделать ScriptableObject со списком префабов-сервисов, которые он инициализирует лениво при запросе. Так его будет проще внедрять.
- В юнити в принципе можно использоват ScriptableObject + сериализацию (SerializeField) как встроенный DI. Хорошо работает для общих сервисов, настроек вьюшек и многих других целей
- Можно оптимизировать локатор (впрочем не уверен, для маленького списка сервисов список должен быть быстрее, нужно тестировать)

public sealed class ServiceLocator {

  private readonly Dictionary<Type, object> services = new Dictionary<Type, object>();
  public void AddService(object service)
  {
      this.services.Add(service.GetType(), service);
  }
  
  public void RemoveService(object service)
  {
      this.services.Remove(service.GetType());
  }
  
  public T GetService<T>()
  {
      if (services.TryGetValue(typeof(T), out var service)) {
          return (T)service;
      }
  
      throw new Exception($"Service of type {typeof(T)} is not found!");
  }
}


- Так же можно обобщить его через Generic интерфейсы - сделать специальный интерфейс Construct<T> и проверять из ассемблера, поддерживает ли класс один из списка.
И прописать списочком зависимости (в этом случае нужно будет писать множество конструкторов для каждого типа данных, по сути это скорее внедрение через поля) либо множественный Construct<A, B>, Construct<A, B, C>, Construct<A, B, C, D> (в этом случае можно будет написать нормальные конструкторы вплоть до 5 параметров, но чтобы нормально их внедрить, все равно нужна рефлексия, чтобы найти в проекте все классы, которые поддерживают варианты конструкторов и использовать только нужные из списка, потому что все варианты проверить нереально)
Правда статическую компиляцию всего этого добра обеспечить довольно трудно, но можно попытаться через лямбды.



У меня не было цели избавиться от DI, так как проблема внедрения зависимости как была так и есть... Я просто хотел показать, что можно реализовать паттерн Dependency Injection и без Reflection. Тут вы правы, что следующим шагом может быть унификация assembler'a через Reflection)))

По поводу реализации ServiceLocator'а через ScriptableObject немного странно, гораздо лучше тогда уж делать Scriptable инсталлеры ServiceLocator'а, которые и будут его инитить)))

По поводу Dictionary под капотом локатора может быть несколько проблем. Например, что если из сервис локатора нужно взять все экземпляры, которые реализуют интерфейс IGameSaveable или в каком-то случае мы хотим получить сервис квестов по интерфейсу, например, IQuestManager? Поэтому более универсальным решением будет List

А можно не заниматься ерундной, взять готовый контейнер и на этом успокоиться. Долго сам использовал сервис-локатор самописный, пока не взял Zenject. После этого понял, что пока у вас нет конкретной причины отказаться от готового DI-Container'a, делать этого не надо.

Причина есть, и она заключается в том, что у разработчика всегда должен быть контроль над системой и возможность затачивать архитектуру под нужны проекта. Если мы возьмем тот же самый Zenject и VContainer, то увидим, что VContainer более производительный и оптимизированный по памяти чем Zenject, но менее user friendly (тут сравнение https://vcontainer.hadashikick.jp/)

Смысл не в том, чтобы переизобретать "велосипед", а в том, чтобы уметь делать кастомную архитектуру под проект, команду, сроки и требования оптимизации. Идея статьи заключается в том, что не существует серебряных пуль как Zenject или VContainer, но есть общие проблемы, которые нужно решить при проектировании системы. И моя задача в этой статье — показать, как эти проблемы нужно решать с помощью различных паттернов и шаблонов)

Согласен с тем, что если в команде нет разработчиков с опытом архитектуры, то лучше брать готовое решение Zenject или VContainer :)

VContainer ни капли не менее User Friendly на мой взгляд. И на мой взгляд, прежде чем делать кастомную архитектуру, нужно собраться командой и очень хорошо ответить на вопрос, почему не использовать Zenject или VContainer. И только после того, как ответ будет дан, причем не шаблонными ответами из интернета, а действительно из опыта разработчиков, которые имели опыт с вышеперечисленными решениями, приступать к разработке своего решения. Потому что кастомное решение это всегда время на поддержку и разработку, сложности с интеграцией новых членов команды и, будем откровенны, зачастую просто хотелка коллектива поиграть в архитекторов.

Откровенно да! Хотелка, которая дает +10 к нематериальной мотивации пилить проект ?

Sign up to leave a comment.