Пост

DI or not DI that's the question

- Каждый программист должен написать свой сервис локатор. (c) Кто-то

Про Zenject

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

  • В нем куча ненужного инструментария
  • Много внутреннего боксинга вэлью типов
  • Рефлексия. Много рантайм рефлексии (даже учитывая, что они в JIT платформах генерируют некоторый код)

Постепенно я отказывался все от большего количества фич этого фрэймворка. Пилил костыли там, где мне не хватало функционала… и понял что я могу написать свое. Нет не так. Я хочу написать свое!

Меня это заинтересовало, но вынашивал я эту идею больше года. И вот взялся. Если все получится, то мой фрэймворк Neonject.DI (именно так он будет называться) можно будет найти на гитхабе. Пока же все это живет в приватных репках и не готово к демонстрации.

Какие проблемы я пытаюсь решить или нивелировать

Рефлексия при инъекции данных в классы

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

В качестве примера. Если у нас есть класс:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public partial class ShowCase: MonoBehaviour
{
    [Inject]
    private MonoBehaviour injectedField;
    [Inject]
    private Renderer InjectedProp { get; set; }

    [InjectTarget(ProcessBeforeInject = true)]
    public Camera injectableBefore;
    [InjectTarget]
    public Camera injectableAfter;

    [Inject]
    public virtual void InjectVirtualMethod(List<IDisposable> disposables)
    {
    }

    [Inject]
    public void InjectSimpleMethod()
    {
    }
}

Для него будет сгенерирован вот такой код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
partial class ShowCase : IInjectable
{
    [Obsolete("Generated code. For Neonject.DI internal use only.")]
    private bool generatedIsInjected;

    [Preserve]
    [Obsolete("Generated code. For Neonject.DI internal use only.")]
    protected static class GeneratedNeonjectInjector
    {
        private static readonly IReadOnlyList<string> method0ArgNames = new List<string>
        {
        "disposables",
        }.AsReadOnly();


        [Preserve]
        [Obsolete("Generated code. For Neonject.DI internal use only.")]
        public static void Process(ShowCase injectable, Container container)
        {
            
            if (injectable.generatedIsInjected) return;
            injectable.generatedIsInjected = true;

            // Deep injection data to fields
            {
                container.Inject(injectable.injectableBefore);
            }

            // Inject data to fields
            {
                var context = ResolveContextData.Field(injectable, "injectedField", injectable.injectedField, false, null);
                injectable.injectedField = container.Resolve<G_NS_2.MonoBehaviour>(context);
            }

            // Inject data to properties
            {
                var context = ResolveContextData.Property(injectable, "InjectedProp", injectable.InjectedProp, false, null);
                injectable.InjectedProp = container.Resolve<G_NS_2.Renderer>(context);
            }

            // Call injection methods
            {
                var context = ResolveContextData.Method(injectable, "InjectVirtualMethod", "disposables", method0ArgNames, false, null);
                var disposables = container.Resolve<G_NS_3.List<System.IDisposable>>(context);
                injectable.InjectVirtualMethod(disposables);
            }
            {
                injectable.InjectSimpleMethod();
            }

            // Deep injection data to fields
            {
                container.Inject(injectable.injectableAfter);
            }
        }
    }

    [Preserve]
    void IInjectable.Inject(Container container)
    {
#pragma warning disable CS0612, CS0618
        GeneratedNeonjectInjector.Process(this, container);
#pragma warning restore CS0612, CS0618
    }
}

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

Из минусов, требуется пометка класса как partial, но это не критичное зло. Система дает возможность регистрировать свои атрибуты для всех необходимых пометок. Основной принцип, которого я придерживаюсь в написании утилиты (“Чтоб удалить DI надо удалить сам пакет и код инсталлеров”) сохраняется. Никто не заставляет тянуть в рабочие классы зависимости.

Рефлексия при создании экземпляров

В процессе разрешения зависимостей контейнеру часто нужно создавать сущности. И тут два пути… для каждого биндинга человеку нужно регистрировать факторку. Мы генерируем внутри факторки сами и используем их, оставляя человеку возможность подсунуть свою. Обычно факторки генерируется рефлексией. На JIT платформах они компилируются в делегаты и работают достаточно шустро. На AOT c этим есть некоторые проблемы. Следовательно мой фрэймворк должен уметь генерировать факторки на этапе компиляции (кодоген, постпроцессинг dll и так далее). И не просто их генерировать, но и регистрировать в системе без участия человека.

Генерация факторок для всех классов проекта - идея интересная, но через чур расточительная. Время компиляции сильно увеличится, размер сборок неоправдано раздуется, а из всего зоопарка факторок использоваться будет дай бог процентов 10. Хотя, если генерировать вообще все, технически задача очень проста. В общем не наш путь.

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

  1. Для всех типов, внутри которых есть элементы помеченные через [Inject] будут созданы факторки.
  2. Полный отказ от передачи в API биндинга типов как аргументов. Только дженерик методы. Причина: на уровне компиляции и постпроцессинга можно отследить с каким типом был вызван данный метод.
    1
    2
    3
    4
    
     //Пример как плохо и чего не будет в API
     builder.Add(typeof(MyClass)).As(typeof(IMyService))
     //Как будет в API
     builder.Add<MyClass>().As<IMyService>()
    
  3. Все методы, которые биндят какой либо тип как источник экземпляра для итоговых сервисов должны быть помечены специальным атрибутом с указанием того, какой (или какие) дженерик параметры отвечают в нем за биндинг. Находя использование таких методов в сборках система будет знать, какие типы вероятно будут инстанцированы в рантайме.
    1
    2
    3
    4
    5
    6
    
     [TypeRegisterMethod("TInstance1", "TInstance2")]
     void MyCustomAddMethod<TInstance1, TInstance2>()
     {
     builder.Add<TInstance1>();
     builder.Add<TInstance2>();
     }
    
  4. При шаге кодогенерации производится поиск использования таких методов и создаются нужные факторки. Не учитывается при этом наследование и возможность использования интерфейсов. Поиск ведется по конкретным методам. Если необходимо использовать интерфейс… нужно пометить метод интерфейса.
  5. Если же все таки какой то тип проскочил… ну что-ж, генерируем факторку на лету посредством рефлексии… или JIT генерации, если платформа это позволяет.

На каком все этапе

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

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

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

Авторский пост защищен лицензией CC BY 4.0 .