Всем привет.

При конструировании приложений хорошим тоном является использование Dependency Injection(внедрение зависимостей). Данный подход позволяет делать код слабо связанным, а это в свою очередь обеспечивает легкость сопровождения. Также облегчается тестирование и код становится красивым, универсальным и заменяемым. При разработке наших продуктов с самого начала использовался этот принцип: и в высоконагруженной DSP и в корпоративном Hybrid. Мы писали модули, подключали интеграцию с различными системами, количество зависимостей росло и в какой-то момент стало сложно поддерживать само конфигурирование приложения. Плюс к этому добавлялись неявные регистрации(например, кастомный DependencyResolver для Web Api задавался в настройках Web Api) и начали возникать сложности с порядком вызова модулей конфигурации. В конце концов мы выработали подход для регистрации, конфигурации и инициализации модулей в сложном приложении. О нём и расскажу.

image

Для начала надо уточнить, что для обслуживания различных задач(даже в рамках одного продукта) у нас работает несколько типов приложений: сервисы, консольные приложения, asp.net. Соответственно система инициализации везде представляла свой зоопарк, единый только в том, что был класс DependencyConfig с чертовой тучей зависимостей на вкус и цвет. Также в каждом из приложений были свои дополнительные настройки. Например, настройка роутинга, конвертеров, фильтров авторизации в asp.net mvc, которая должна была вызываться после регистрации зависимостей и проверки корректности данной регистрации. Соответственно встала задача:

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

В итоге мы выделили 3 типа элементарных конфигураций: зависимости(dependency), инициализации(init) и настройки(settings, которые на самом деле объединение двух предыдущих).

Зависимости(IDependency)


Зависимость представляет собой примитив для регистрации, ха-ха, зависимостей одного модуля. В общем случае реализует интерфейс IDependency:

public interface IDependency<TContainer>
{
    void Register(TContainer container);
}

где TContainer — IoC-контейнер(В качестве примера контейнера здесь и далее используется SimpleInjector). Соответственно в методе Register регистрируются сервисы одного логического модуля. Также могут регистрироваться другие IDependency-примитивы посредством прямого вызова конструктора и метода Register. Пример:

public class TradingDeskDependency : IDependency<Container>
{
    public void Register(Container container)
    {
          container.Register(() => new SwiffyClient(new SwiffyOptions{ MillisecondsTimeout = 20000 }));
          new DspIntegrationDependency().Register(container);
    }
}

Инициализации(IInit)


Инициализации включают в себя тот код, который должен выполнять после регистрации и проверки зависимостей, но до старта основной логики приложения. Это может быть настройка asp.net mvc и web api или что-то подобное. В общем случае класс инициализации реализует интерфейс IInit:

public interface IInit
{
    void Init(IDependencyResolver resolver);
}

гдe IDependencyResolver нужен, если требуется получение какого-нибудь сервиса из зависимостей, либо для получения самих методов получения зависимостей, как в примере:

public class AspNetMvcInit: IInit
{
    public void Init(IDependencyResolver resolver)
    {
        System.Web.Mvc.DependencyResolver.SetResolver(resolver.GetService, resolver.GetServices);

        new RouteInit().Init(resolver);
    }
}

Так же, как и для зависимостей можно использовать вложенные примитивы инициализации.

Настройки(ISettings)


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

 public interface ISettings<TContainer> : IDependency<TContainer>, IInit
 {
 }

Соответственно, именно настройки представляют полную функциональность для конфигурирования логического модуля: как регистрацию зависимостей, так и дополнительные настройки.

Общая конструкция


Итак, у нас есть примитивы, на которые можно разбить конфигурацию, осталось настроить управление ими. Для этого нам поможет класс Application, реализующий интерфейс IApplication:

public interface IApplication<TContainer>
{
    IApplication<TContainer> SetDependency<T>(T dependency) where T : IDependency<TContainer>;

    IApplication<TContainer> RemoveDependency<T>() where T : IDependency<TContainer>;

    IApplication<TContainer> SetInit<T>(T init) where T : IInit;

    IApplication<TContainer> RemoveInit<T>() where T : IInit;

    IApplication<TContainer> SetSettings<T>(T settings) where T : ISettings<TContainer>;

    IApplication<TContainer> RemoveSettings<T>() where T : ISettings<TContainer>;

    IAppConfig Build();
}

Как видно из кода, IApplication позволяет добавлять все типы настроек(а также удалять их). А метод Build вызывает код, собирающий всё эти настройки: сначала выполняется регистрация зависимостей(+ если нужно — проверка, возможно ли всё зарегистрировать), далее — код из IInit-модулей(и методов Init в ISettings). На выходе получаем объект IAppConfig:

public interface IAppConfig
{
    IDependencyResolver DependencyResolver { get; }

    IAppLogger Logger { get; }
}

где DependencyResolver позволяет получать сервисы, а Logger сами знаете для чего. Итоговый код для настройки приложения будет прост и прозрачен(хотя в общем случае с некоторыми усложнениями для универсальности):

var container = new Container()

var appOptions = new AppOptions
{
    DependencyContainer = container,
    GetServiceFunc = container.GetInstance,
    GetAllServicesFunc = container.GetAllInstances,
    VerifyAction = c => c.Verify(),
    Logger = new CustomLogger()
};
var appConfig = new Application(appOptions).SetDependency(new TradingDeskDependency())
                                           .SetInit(new AspNetMvcInit())
                                           .Build();

Единственный класс, который придется определять явно — это CustomLogger. Если мы хотим отслеживать ситуации, когда регистрация зависимостей и инициализаций валится с ошибкой, то задать его следует. Логгер описывается простейшим интерфейсом:

public interface IAppLogger
{
    void Error(Exception e);
}

и написать реализацию не составит труда.

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

Я написал немного упрощенный(но вполне рабочий и легко расширяемый) вариант библиотеки(Jdart.CoreApp), каковой можно изучить или просто использовать:
1) GitHub
2) Nuget.

Также доступны адаптеры для
1) SimpleInjector
2) Autofac
3) Ninject
4) Unity

Всем спасибо.

Комментарии (11)


  1. SerrNovik
    17.09.2015 14:30
    +2

    А в чем преимущество такого собственного контейнера, перед тем чтобы взять один из многих доступных вариантов, например Unity, Ninject и т.д? Вроде все предоставляют широкий набор где конфигурировать зависимости, в коде, в библиотеке, в xml, комбинацию вариантов и т.д.?


    1. Alvaro
      17.09.2015 14:45

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


      1. SerrNovik
        17.09.2015 15:03
        +3

        А какой именно функциональности не хватает в широко используемых IoC контейнерах?

        Насколько я знаю практика удаления зависимостей (RemoveDependency) после конфигурации считается плохой нарушающей целый ряд принципов, основной из которых в том что контейнер создается только один раз в application root точке одной строчкой кода. Если нужно несколько комбинаций зависимостей тогда используется например контекстуальные зависимости.

        Или я что-то неправильно понял из целей?


        1. Alvaro
          17.09.2015 15:22

          Во-первых, удаление(RemoveDependency) не пересоздает контейнер и никаким образом не удаляет зависимости из него. Просто на этапе сборки(Build) некий модуль не будет зарегистрирован(и его зависимости).

          По поводу функциональности IoC-контейнеров. Приведу пример:
          Нам нужно сконфигурировать в asp.net приложении часть asp.net mvc-фреймворка: роутинги, конвертеры, биндеры, фильтры, в том числе фильтры авторизации. Фильтр авторизации нам надо зарегистрировать в IoC-контейнере. А после регистрации всех зависимостей нам надо на этапе настройки mvc добавить сей фильтр в коллекцию фильтров. Подход с созданием settings-модуля позволяет объединить в рамках одного класса как регистрацию фильтра так и его получение с последующим добавлением в коллекцию фильтров(причем в рамках данного класса мы не знаем, когда все зависимости будут зарегистрированы и проверены). Стандартные же функции IoC-контейнера способны оперировать только уровнем регистрации, но не пост-обработки.


          1. SerrNovik
            17.09.2015 15:33
            +1

            Фильтры поддерживаются, в том числе какие угодно сложные типа Select(Func<Type, bool> filter) пример из Ninject документации. Реализуется это не ядром а расширением, Ninject.Extensions.Conventions но функционал этот есть. В других контейнерах есть аналоги. Это не тоже самое?


            1. Alvaro
              17.09.2015 15:48

              Так. Насколько я понимаю, вы думаете, что я написал свой IoC-контейнер. Это не так. С помощью моего подхода можно использовать любой существующий контейнер(тот же Unity или Autofac). А мой пример поясню кодом(под фильтрами я подразумевал фильтры asp.net mvc: FilterAttribute):

              public class MvcSettings: ISettings<Container>
              {
                public void Init(IDependencyResolver resolver)
                {
                    GlobalFilters.Filters.Add(resolver.GetService<CustomAuthorizeFilter>())      
                }
                
                public virtual void Register(Container container)
                {
                    container.Register(() => new CustomAuthorizeFilter("params"))
                }
              }
              


              Код максимально упрощен, но он показывает назначение подхода: в одном классе мы и регистрируем фильтр, и получаем его для сохранение в коллекции фильтров.


              1. SerrNovik
                17.09.2015 16:15
                +1

                Да, мне казалось что это свой контейнер, видимо плохо разобрался.

                Но понятно пока не до конца. Т.е. проблема в том, что на момент инициализации веб приложения не известно какие типы надо будет использовать и их надо динамически подгружать откуда-то (как плагины)? Кажется маловероятным для веб приложения.

                Просто если набор зависимостей конечен и известен их можно зарегистрировать и уже какая-нибудь фабрика будет нужные типы подбирать? Или фактически это и есть такая фабрика?

                Я просто не очень понимаю зачем надо динамически регистрировать зависимости если набор зависимостей известен на момент компиляции (а в случае mvc инициализации) приложения. Я всегда видел рекомендации не делать регистрацию нигде кроме как в одной корневой точке.


                1. Alvaro
                  17.09.2015 16:26

                  Основная проблема возникает, когда с одной стороны зависимостей реально много(хоть и конечное количество), а с другой стороны кроме регистрации зависимостей есть ещё дополнительный код инициализации(как в моем примере). Попытка делать всё в единой точке(с чего мы и начинали) с ростом проекта приводит к тому, что контролировать код в этой единой точке становится сильно неудобно + надо как-то контролировать этот пост-регистрационный код. Цель — легкое управление модулями(все их регистрации, настройки, записи в глобальные коллекция типа коллекции фильтров), чтобы можно было очень просто найти модуль, изменить его настройки или вовсе удалить, не разбираясь в куче не относящегося к нему кода. Именно для этого всё и сделано.


                  1. SerrNovik
                    17.09.2015 16:44

                    Теперь вроде понятно. Всегда интересно было узнать другие подходы.

                    Хотя мне все — равно кажется что было бы удобнее разбивать и структурировать модули в регистрации до тех пор пока код не станет удобным для поддержки.


                    1. Alvaro
                      17.09.2015 16:50

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


  1. SerrNovik
    17.09.2015 15:02
    +2

    [del]