Comments 18
Мне кажется стоило назвать цикл статей в духе "Отказываемся от zenject и пишем свой велосипед", оно бы лучше отражало суть написаного. Но я так и не увидел мотивационную часть, а зачем? Какие преимущества важные для данного проекта мы получили отказавшись от библиотеки, которая является в некотором смысле индустриальным стандартом di-фреймворка в юнити и написав свой велосипед? Во имя чего всё это?
Привет! Спасибо за фидбек)
На мой взгляд, архитектура не может быть велосипедом, потому что она делается под конкретный проект в определенных условиях (сроки, команда, бюджет, технологии и т.д.)
Одна из ключевых идей, которую я хотел донести в статье это то, что разработчики сами могут построить свою простую архитектуру и заточить ее под без использования фреймворков Zenject или VContainer
Почему мне фреймворки не нравятся:
Во первых, они построены на инверсии управления и забирают у разработчика возможность управлять потоком исполнения. И если тебе нужно перестроить ход инициализации приложения, то это может очень быть проблематично.
Во-вторых, если говорить конкретно про Zenject, то для меня это выглядит сложно. Нужно потратить немало времени на изучение фреймворка прежде чем приступать к реализации. Зачем это все: 1000 способов биндингов, различные контексты, встроенные фабрики, пулы, интерфейсы IInitializable, ITickable, если это все мне может не понадобиться в общем случае?
Поэтому для меня гораздо проще будет сделать свое решение по принципу KISS (Keep It Simple Stupid), которое будем понятным и простым для разработчиков. И заложить в архитектуру только те опции, которые будут нужны в проекте. И если нужно управлять ими или оптимизировать.
Уже говорил неоднократно, что "серебряных пуль не бывает"!
Надеюсь, ответил на вопрос. Если в команде нету опыта проектирования архитектуры, то берите Zenject или VContainer :)
Я бы пожалуй возразил про:
Во первых, они построены на инверсии управления и забирают у разработчика возможность управлять потоком исполнения. И если тебе нужно перестроить ход инициализации приложения, то это может очень быть проблематично.
Так как управлять потоком выполнения на самом деле мешает Юнити и неопределённость вызова методов у MonoBehaviour. Как раз это и корень проблемы. А например конструкторы - не важно кем выполненные, не являются такой проблемой. И не важно, выполняет их контейнер активатором или сам рантайм.
И действительно, в Zendject написано 90% бесполезного мусора, который только мешает понимать суть проблемы, тк этот сахар живёт по правилам убогого фреймворка. Но сама суть Di от этого хуже не становится, просто нужно использовать инъекцию только в конструктор и абстрактные фабрики. Для всех случаев в разработке игр этого будет достаточно. Единственный ООП кейс в котором будет неудобно, это параметризированный конструктор, где сигнатура зависит от типа. Такое скорее всего можно обойти абстрактным методом Init(x, y) у общего типа, и скрыть внутри своей фабрики, или наоборот, сделать пост-иньекцию в метод, а сами конструкторы сделать одинаковыми.
По поводу конструкторов и методов пост-инъекции вопросов нет. Тут речь идет про то, что должна быть единая точка входа в программу, и разработчик решает в какой момент и куда и как делать инъекцию, а не так, что через Script Execution Order фреймворк сам побежал "шурудить" все игровые объекты по сцене через рефлексию
Всё так, но почему в примерах статьи так много наследников от MonoBehaviour когда это обычные классы которые нуждаются в конструкторе?
Не нужно иметь под рукой сервис локатор или метод GetService, если в конструктор всё приходит, и выстроено в правильном порядке. А то что накликано в юнити мышкой, ну фиг знает, скорее всего DI нарушен многократно.
Хм, к концу статьи у нас получилось всего 4 монобеха в системе)
Вот ссылка на код-базу
----------
Монобехами являются GameContext, GameLauncher, PlayerInstaller & InputInstaller, а вся игровая логика сделана на обычных классах
GameContext — монобех, чтобы можно было вручную делать старт/паузу игры и видеть стейт
GameLauncher — монобех, чтобы запускать игру с помощью списка задач, которые можно выставлять в инспекторе
А инсталлеры — PlayerInstaller & InputInstaller чтобы можно было через инспектор видеть состояния классов и дебажить их (Но можно их сделать и ScriptableObject'ами)
----------
По поводу сервис-локатора согласен, что класс, который выполняет бизнес-логику не должен иметь зависимость на ServiceLocator, а должен получать зависимости через конструктор или в метод пост-инъекции. И если у тебя есть инсталлеры, которые хранят классы с бизнес-логикой, то ты можешь поручить им ответственность за создание экземпляров этих классов и внедрения зависимостей.
Просто внедрение зависимости не ограничивается получением сервисов из ServiceLocator'а. Про накликано в юнити мышкой пока что не оч понял, скорее всего речь про инспектор)
Все еще довольно сомнительно.
Не понимаю кто в здравом уме между вариантами "костылить свой фреймворк ради IoC" и "посмотреть 5-минутное видео про Zenject" выберет второе.
Если опять же у нас архитектура делается под "сроки, команду, бюджет и технологии", то в каком варианте этих составляющих, предпочтение сделается в пользу создания своего сервис локатора, который пытается быть похожим на di вместо использования "индустриального стандарта di-фреймворка"?)
Я бы понял еще если бы шел разговор о том, что "вот Zenject не подходит, у него нет такого-то списка фичей, которые нам тут нужны", но тут мы имеем всего лишь пример с кубиком и по сути вся речь о том "как сделать тоже самое, только хуже и свое".
Готов поспорить, что мое решение отработает быстрее чем Zenject
А никто не говорит про скорость работы. Да и козырять ей на примере с кубиком очень уж сомнительно) Уверен, что после 5 лет разработки можно понять, что нет смысла хвастаться из-за сэкономленной миллисекунды, особенно если потратил гораздо больше времени, костыля свой фреймворк) Хотя я бы с радостью посмотрел на эти тесты
Ну и для кучи можно еще VContainer протестировать, раз уж мы вдруг заговорили про скорость)
Когда я писал первую часть статьи, я обозначил проблемы, которые нам нужно решить. Напротив каждой задачи буду ставить плюсик, есть ли эта фича в Zenject:
Механизм внедрения зависимостей (+)
Оптимизация архитектуры и уход от монобехов (+)
Единая точка входа в приложение (-)
Порядок инициализации игры (-)
Работа с состояниями игры (старт/пауза/завершение) (-)
Оповещение компонентов системы об изменение состояния игры (+-)
Суть в том, что в основном Zenject решает проблему внедрения зависимости, но под оставшиеся задачи нужно все-равно писать кастомный код... И поэтому, я считаю, что если есть возможность выстроить целостную инфраструкуру под решение всех задач, то в долгосрочной перспективе это будет лучше, чем потом думать, а как это можно состыковать с выбранным фреймворком
Мне кажется что 3 и 4 пункты как раз в Zenject есть. Это момент когда у рутового объекта будет вызван единственный Resolve. Этот объект и может иметь единственную точку входа и единственный Update() метод во всём проекте, и дальше работать уже со своим деревом просто как c C# классами.
А вот как раз 5 и 6 и не должны быть в функционале контейнера, и если в сахаре Zenject это есть, то использовать то не стоит.
И да и нет :)
Метод Resolve не может быть единственным, поскольку у каждого контекста инициализация и разрешение зависимости в своём Di контейнере происходит в разные моменты выполнения программы. Например, у ProjectContext'а в момент его создания, у GameObjectContext'а — в методе Construct, у SceneContext'а — в Awake() (причем первый в ScriptExecutionOrder'е)
На мой взгляд, это больше относиться к автоматическому механизму разрешения зависимостей с большим набором опций, но к процессу инициализации игры это никак не относиться, так как процесс инициализации может в себя включать соединение с сервером и загрузку пользовательских данных, и вообще может быть асинхронным
---
По поводу Update(), он тоже не единственный, так как этот метод вызывается у каждого MonoKernel, который крепиться дополнительно к каждому гейм-обджекту, на котором висит компонент ProjectContext / SceneContext / GameObjectContext
---
5 и 6 пункт, согласен :)
Управления жизненным циклом приложения и создание точки инициализации можно через бутстрап сцену сделать. Зенжект тут ничем не мешает совершенно.
По поводу того, что в зенжекте много функционала я согласен, но не согласен с тем, что это плохо. Большая часть перечисленного не обязательна к использованию, но если оно понадобилось то уже под рукой готовое и протестированное на сотнях проектов.
Ну и я вцелом не согласен что лучше писать свой велосипед на любой чих потому что он комуто-там (прежде всего автору) понятен. Прелесть использования широко распространенных библиотек и фреймворков как раз в том что новый разработчик если знаком с тем же зенжектом может сразу плюс минус идти в бой, даже на относительно крупном проекте. Но нет никаких шансов нанять инженера который раньше уже катался на всех ваших велосипедах, которые вы напишете для реализации давно решенных задач.
Да, тут я согласен, что популярность фреймворков упрощает интеграцию новых разработчиков в проект, и это, действительно, оправданный выбор использовать Zenject с точки зрения бизнеса.
С другой стороны надо понимать, что Zenject — это универсальное решение и под капотом оно делает много телодвижений, которые в вашем проекте могут просто не понадобиться.
Таким образом, вы можете сделать все то же самое, и это будет работать быстрее, проще и компактнее, потому что вы взяли только то, что вам нужно для вашего проекта и отсекли все лишнее :)
А вопрос велосипедов — это вопрос опыта разработки архитектуры
И если в команде нет такого опыта, то лучше делегировать ответственность фреймворку и не париться ?
Нет никаких проблем с ходом инициализации приложения. Bootstrap + StateMachine для контроля цикла жизни приложения решают все проблемы. Во всяком случае я себе не могу представить когда это могло бы не хватить
Кому мешает "лишний" функционал Zenject, берет VContainer. О каком не малом времени идет речь на изучение? Любой мало-мальски нормальный разработчик через пару часов с докой поймет основные принципы. Только недавно решил в новом проекте использовать VContainer, и через пару часов с докой я понял практически все, что мне надо на данный момент. Плюс изучив в одной фирме Zenject или VContainer, я смогу применять эти знания в любой другой. Кастомный же велосипед при переходе в другую команду это просто мусорные знания.
Вы пишите что все эти ITickable лишние, но при этом сами же вводите IUpdateListener. Так может не такие уж они лишние?
Короче, очередная кастомная архитектура, которая не нужна.
Спасибо за статью! Почитал и предыдущие части тоже. Полностью согласен с автором по поводу громоздкости и ненужного функционала зенджекта. Полностью согласен, что всегда лучше писать решение под проект. И всегда придерживаюсь правила написания своего велосипеда, так как в этом велосипеде нет ничего лишнего, все понятно как работает, и не будет неожиданных багов и ограничений других фреймфорков, которые будет крайне тяжело исправить. Любителям зенджекта привет от веяния времени, ведь завтра вы уже перейдете на другую хрень, которая будет популярна.
Архитектура на Unity без Zenject. Часть 3