GameDev: разработка на Unity3D
443 subscribers
6 photos
2 videos
7 files
40 links
Все для успешного развития разработчика в геймдев: от junior до CTO. SOLID на практике, IOC, DI. Новые паттерны для gamedev. Личный опыт.

Заявка на разбор тестовых https://forms.gle/kqVPv1jWT97Bkps9A
Download Telegram
#архитектура #dependency_injection #di #zenject

Приветствую, друзья 👋

Сегодня говорим про Dependency Injection (DI) и DI фреймворки.

Статью дробим на 3 части:
1. Рассмотрим самый популярный фреймворк Zenject, обозначим недостатки.
2. Выделим достоинства Zenject и дадим общие рекомендации по выбору фреймворка.
3. Проиллюстрирую ряд обозначенных проблем примерами кода.

Итак, поехали 🚀

Самый распространенный и популярный DI фреймворк - Zenject, существует с 2014 года.
Zenject используется как независимыми разработчиками, так и крупными игровыми студиями, среди которых: Square Enix, Electronic Arts, Ubisoft, Zenimax Online Studios и многие другие.

Zenject - будет основной линией данной статьи, на нем будем приводить примеры.

Одной из критик Zenject является высокий уровень абстракции и высокий порог входа для новичков.

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

1. отсутствие полного контроля над жизненным циклом объектов.
Zenject основывается на автоматическом внедрении зависимостей и может привести к утечкам памяти при некорректном уничтожении объектов

2. проблемы с настройкой очередностей в MonoInstaller Zenject контекста.
Zenject использует собственный механизм внедрения зависимостей и контроля жизненного цикла объектов, который может конфликтовать с механизмами Unity

3. усложнение настройки зависимостей и снижении производительности из-за создания большого количества объектов и вызова методов на каждый из них (особенно в коде с большим количеством зависимостей)

4. настройка проекта. Настройка складывается из двух частей: через GUI Unity и через код. Настройка через GUI опасна, так как может повредить код, что не всегда очевидно даже для опытных разработчиков. Исправление занимает много времени. Для новичков такая настройка особенно сложна и затруднительна в освоении

5. неявное внедрение зависимости через атрибут [Inject]

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

Недостатки фабрик, предоставляемых Zenject:
- невозможность создания нескольких экземпляров одного и того же компонента с различными параметрами
- ограниченная возможность создания экземпляров, которые требуют сложной логики инициализации
- ограничения на наследование MonoBehaviour
- ограничения, связанные с созданием экземпляров внутри сцены

7. усложнение процесса обновления Unity до новых версий из-за потенциальных несовместимостей с новыми API

8. портирование Zenject проекта на другие фреймворки.


Продолжение 👇
#архитектура #dependency_injection #di #zenject


Преимущества Zenject:

1. инверсия управления (IoC) и внедрение зависимостей (DI) помогают ослабить зацепление классов и улучшить тестируемость кода

2. поддержка скоупов (при условии их корректного использования) позволяет управлять жизненным циклом объектов и оптимизировать использование ресурсов

3. интеграция с Unity позволяет использовать DI в контексте игровых объектов и компонентов

4. использование интерфейсов в Zenject позволяет значительно повысить уровень абстракции в проекте.
Разделить проект на слои абстракции, каждый из которых будет содержать свои интерфейсы и реализации. Изменения в одном слое не будут затрагивать другие слои.
Кроме того, использование интерфейсов позволяет легко заменять реализации во время тестирования и создавать мок-объекты для модульного тестирования.

Zenject оптимален для слаженных и опытных командам с малой текучкой кадров и высоким уровнем компетенции.

Общие советы при выборе DI фреймворка:

- опирайтесь на потребности проекта и компетенцию команды разработчиков

- помните, использование DI фреймворков не всегда обязательно, зависит от целей проекта и его масштаба. Например, разработка прототипа не всегда требует использования DI фреймворка, так как направлена на быстрое создание простой модели продукта для проверки гипотез и его концепции

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

- учитывайте наличие документации и наличие сообщества у выбранного фреймворка

- протестируйте выбранный DI фреймворк перед его использованием в реальном проекте.

Использование DI фреймворков может упростить разработку, облегчить поддержку и улучшить переносимость кода между разными проектами.

Жду вопросы в комментариях!

В следующей публикации приведу примеры-иллюстрации проблемных мест Zenject👇
#архитектура #dependency_injection #di #zenject #код


Рассмотрим пример кода проблемных мест на базе Zenject обозначенных в предыдущей публикации.

Пример 1. Проблемы с производительностью

public class SomeClass
{
private readonly SomeDependency _dependency;

public SomeClass(SomeDependency dependency)
{
_dependency = dependency;
}

public void DoSomething()
{
// use _dependency
}
}

public class SomeDependency
{
public SomeDependency()
{
// do something expensive
}
}

public class SomeInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<SomeClass>().AsSingle().NonLazy();
Container.Bind<SomeDependency>().AsSingle().NonLazy();
}
}


В рассмотренном примере проблема с производительностью возникает при создании объектов с зависимостями во время выполнения программы. Конструирование объекта SomeDependency в SomeClass является дорогостоящей операцией, особенно если объект создается многократно в рамках работы приложения.

Одно из возможных решений проблемы производительности - использование объекта SomeDependency в виде синглтона, созданного во время запуска приложения, и использование его в качестве зависимости в SomeClass. Подход позволит избежать повторного создания объекта и улучшить производительность приложения.


Пример 2. Проблемы с циклической зависимостью

public class SomeClass : MonoBehaviour
{
[Inject] private SomeDependency _dependency;

private void Start()
{
_dependency.DoSomething();
}
}

public class SomeDependency
{
private readonly SomeOtherDependency _otherDependency;

public SomeDependency(SomeOtherDependency otherDependency)
{
_otherDependency = otherDependency;
}

public void DoSomething()
{
// use _otherDependency
}
}

public class SomeOtherDependency
{
public SomeOtherDependency(SomeDependency someDependency)
{
someDependency.DoSomething();
}
}

public class SomeInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<SomeDependency>().AsSingle();
Container.Bind<SomeOtherDependency>().AsSingle();
}
}


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

Пример 3. Неявное внедрение зависимости через атрибут [Inject]

public class MyComponent : MonoBehaviour
{
private IMyDependency _myDependency;

public void DoSomething()
{
_myDependency.DoStuff();
}
}

И интерфейс, который определяет эту зависимость:

public interface IMyDependency
{
void DoStuff();
}

То, чтобы внедрить зависимость в MyComponent, нужно добавить атрибут [Inject] к полю _myDependency:

public class MyComponent : MonoBehaviour
{
[Inject]
private IMyDependency _myDependency;

public void DoSomething()
{
_myDependency.DoStuff();
}
}


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

Проблемы использова
ния атрибута [Inject]:

1. Зависимости стан
овятся скрытыми и сложными в управлении. Передаются неявно, что затрудняет понимание, какие зависимости в каких частях кода, и как их переиспользовать.

2. Ошибки времени выполнения.

В случае, если некоторые зависимости не были правильно настроены

3. Низкая гибкость.
Сложно управлять жизненным циклом зависимостей, некоторые операции, например, уничтожение или замена зависимостей, могут быть затруднены.

4. Снижение производительности.
В некоторых случаях требуются дополнительные операции по поиску и инициализации зависимостей.

Благодарим за внимание!
Хороших выходных 😉