Как стать автором
Обновить
0
Targetix
Управлять RTB-рекламой легко!

DI в сложных приложениях. Как не утонуть в зависимостях

Время на прочтение 5 мин
Количество просмотров 10K
Всем привет.

При конструировании приложений хорошим тоном является использование 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

Всем спасибо.
Теги:
Хабы:
+5
Комментарии 11
Комментарии Комментарии 11

Публикации

Информация

Сайт
hybrid.ru
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия

Истории