Всем привет.
При конструировании приложений хорошим тоном является использование Dependency Injection(внедрение зависимостей). Данный подход позволяет делать код слабо связанным, а это в свою очередь обеспечивает легкость сопровождения. Также облегчается тестирование и код становится красивым, универсальным и заменяемым. При разработке наших продуктов с самого начала использовался этот принцип: и в высоконагруженной DSP и в корпоративном Hybrid. Мы писали модули, подключали интеграцию с различными системами, количество зависимостей росло и в какой-то момент стало сложно поддерживать само конфигурирование приложения. Плюс к этому добавлялись неявные регистрации(например, кастомный DependencyResolver для Web Api задавался в настройках Web Api) и начали возникать сложности с порядком вызова модулей конфигурации. В конце концов мы выработали подход для регистрации, конфигурации и инициализации модулей в сложном приложении. О нём и расскажу.
Для начала надо уточнить, что для обслуживания различных задач(даже в рамках одного продукта) у нас работает несколько типов приложений: сервисы, консольные приложения, asp.net. Соответственно система инициализации везде представляла свой зоопарк, единый только в том, что был класс DependencyConfig с чертовой тучей зависимостей на вкус и цвет. Также в каждом из приложений были свои дополнительные настройки. Например, настройка роутинга, конвертеров, фильтров авторизации в asp.net mvc, которая должна была вызываться после регистрации зависимостей и проверки корректности данной регистрации. Соответственно встала задача:
В итоге мы выделили 3 типа элементарных конфигураций: зависимости(dependency), инициализации(init) и настройки(settings, которые на самом деле объединение двух предыдущих).
Зависимость представляет собой примитив для регистрации, ха-ха, зависимостей одного модуля. В общем случае реализует интерфейс IDependency:
где TContainer — IoC-контейнер(В качестве примера контейнера здесь и далее используется SimpleInjector). Соответственно в методе Register регистрируются сервисы одного логического модуля. Также могут регистрироваться другие IDependency-примитивы посредством прямого вызова конструктора и метода Register. Пример:
Инициализации включают в себя тот код, который должен выполнять после регистрации и проверки зависимостей, но до старта основной логики приложения. Это может быть настройка asp.net mvc и web api или что-то подобное. В общем случае класс инициализации реализует интерфейс IInit:
гдe IDependencyResolver нужен, если требуется получение какого-нибудь сервиса из зависимостей, либо для получения самих методов получения зависимостей, как в примере:
Так же, как и для зависимостей можно использовать вложенные примитивы инициализации.
Настройки нужны, если в логическом модуле необходима как регистрация зависимостей, так и вызов инициализации после. Описываются они проще всего:
Соответственно, именно настройки представляют полную функциональность для конфигурирования логического модуля: как регистрацию зависимостей, так и дополнительные настройки.
Итак, у нас есть примитивы, на которые можно разбить конфигурацию, осталось настроить управление ими. Для этого нам поможет класс Application, реализующий интерфейс IApplication:
Как видно из кода, IApplication позволяет добавлять все типы настроек(а также удалять их). А метод Build вызывает код, собирающий всё эти настройки: сначала выполняется регистрация зависимостей(+ если нужно — проверка, возможно ли всё зарегистрировать), далее — код из IInit-модулей(и методов Init в ISettings). На выходе получаем объект IAppConfig:
где DependencyResolver позволяет получать сервисы, а Logger сами знаете для чего. Итоговый код для настройки приложения будет прост и прозрачен(хотя в общем случае с некоторыми усложнениями для универсальности):
Единственный класс, который придется определять явно — это CustomLogger. Если мы хотим отслеживать ситуации, когда регистрация зависимостей и инициализаций валится с ошибкой, то задать его следует. Логгер описывается простейшим интерфейсом:
и написать реализацию не составит труда.
В итоге этот подход можно использовать для любого типа приложения, нам не нужно думать о порядке конфигурирования и в управлении зависимостями мы перешли на уровень логических модулей. Таким образом можно навернуть кучу настроек, зависимостей, инициализаций, сохраняя трезвость мысли и крепость духа.
Я написал немного упрощенный(но вполне рабочий и легко расширяемый) вариант библиотеки(Jdart.CoreApp), каковой можно изучить или просто использовать:
1) GitHub
2) Nuget.
Также доступны адаптеры для
1) SimpleInjector
2) Autofac
3) Ninject
4) Unity
Всем спасибо.
При конструировании приложений хорошим тоном является использование Dependency Injection(внедрение зависимостей). Данный подход позволяет делать код слабо связанным, а это в свою очередь обеспечивает легкость сопровождения. Также облегчается тестирование и код становится красивым, универсальным и заменяемым. При разработке наших продуктов с самого начала использовался этот принцип: и в высоконагруженной DSP и в корпоративном Hybrid. Мы писали модули, подключали интеграцию с различными системами, количество зависимостей росло и в какой-то момент стало сложно поддерживать само конфигурирование приложения. Плюс к этому добавлялись неявные регистрации(например, кастомный DependencyResolver для Web Api задавался в настройках Web Api) и начали возникать сложности с порядком вызова модулей конфигурации. В конце концов мы выработали подход для регистрации, конфигурации и инициализации модулей в сложном приложении. О нём и расскажу.
Для начала надо уточнить, что для обслуживания различных задач(даже в рамках одного продукта) у нас работает несколько типов приложений: сервисы, консольные приложения, 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
Всем спасибо.
SerrNovik
А в чем преимущество такого собственного контейнера, перед тем чтобы взять один из многих доступных вариантов, например Unity, Ninject и т.д? Вроде все предоставляют широкий набор где конфигурировать зависимости, в коде, в библиотеке, в xml, комбинацию вариантов и т.д.?
Alvaro
Собственная обертка нужна не просто для того, чтобы распихать регистрацию зависимостей по блокам, но и для предоставления полного функционала конфигурирования модулей: и регистрацию зависимостей, и и использование этих зависимостей после регистрации в рамках модуля. Ну и в меньшей степени это нужно для поддержки модулей для любого IoC-контейнера(хоть и во многих популярных контейнерах эта функциональность есть)
SerrNovik
А какой именно функциональности не хватает в широко используемых IoC контейнерах?
Насколько я знаю практика удаления зависимостей (RemoveDependency) после конфигурации считается плохой нарушающей целый ряд принципов, основной из которых в том что контейнер создается только один раз в application root точке одной строчкой кода. Если нужно несколько комбинаций зависимостей тогда используется например контекстуальные зависимости.
Или я что-то неправильно понял из целей?
Alvaro
Во-первых, удаление(RemoveDependency) не пересоздает контейнер и никаким образом не удаляет зависимости из него. Просто на этапе сборки(Build) некий модуль не будет зарегистрирован(и его зависимости).
По поводу функциональности IoC-контейнеров. Приведу пример:
Нам нужно сконфигурировать в asp.net приложении часть asp.net mvc-фреймворка: роутинги, конвертеры, биндеры, фильтры, в том числе фильтры авторизации. Фильтр авторизации нам надо зарегистрировать в IoC-контейнере. А после регистрации всех зависимостей нам надо на этапе настройки mvc добавить сей фильтр в коллекцию фильтров. Подход с созданием settings-модуля позволяет объединить в рамках одного класса как регистрацию фильтра так и его получение с последующим добавлением в коллекцию фильтров(причем в рамках данного класса мы не знаем, когда все зависимости будут зарегистрированы и проверены). Стандартные же функции IoC-контейнера способны оперировать только уровнем регистрации, но не пост-обработки.
SerrNovik
Фильтры поддерживаются, в том числе какие угодно сложные типа Select(Func<Type, bool> filter) пример из Ninject документации. Реализуется это не ядром а расширением, Ninject.Extensions.Conventions но функционал этот есть. В других контейнерах есть аналоги. Это не тоже самое?
Alvaro
Так. Насколько я понимаю, вы думаете, что я написал свой IoC-контейнер. Это не так. С помощью моего подхода можно использовать любой существующий контейнер(тот же Unity или Autofac). А мой пример поясню кодом(под фильтрами я подразумевал фильтры asp.net mvc: FilterAttribute):
Код максимально упрощен, но он показывает назначение подхода: в одном классе мы и регистрируем фильтр, и получаем его для сохранение в коллекции фильтров.
SerrNovik
Да, мне казалось что это свой контейнер, видимо плохо разобрался.
Но понятно пока не до конца. Т.е. проблема в том, что на момент инициализации веб приложения не известно какие типы надо будет использовать и их надо динамически подгружать откуда-то (как плагины)? Кажется маловероятным для веб приложения.
Просто если набор зависимостей конечен и известен их можно зарегистрировать и уже какая-нибудь фабрика будет нужные типы подбирать? Или фактически это и есть такая фабрика?
Я просто не очень понимаю зачем надо динамически регистрировать зависимости если набор зависимостей известен на момент компиляции (а в случае mvc инициализации) приложения. Я всегда видел рекомендации не делать регистрацию нигде кроме как в одной корневой точке.
Alvaro
Основная проблема возникает, когда с одной стороны зависимостей реально много(хоть и конечное количество), а с другой стороны кроме регистрации зависимостей есть ещё дополнительный код инициализации(как в моем примере). Попытка делать всё в единой точке(с чего мы и начинали) с ростом проекта приводит к тому, что контролировать код в этой единой точке становится сильно неудобно + надо как-то контролировать этот пост-регистрационный код. Цель — легкое управление модулями(все их регистрации, настройки, записи в глобальные коллекция типа коллекции фильтров), чтобы можно было очень просто найти модуль, изменить его настройки или вовсе удалить, не разбираясь в куче не относящегося к нему кода. Именно для этого всё и сделано.
SerrNovik
Теперь вроде понятно. Всегда интересно было узнать другие подходы.
Хотя мне все — равно кажется что было бы удобнее разбивать и структурировать модули в регистрации до тех пор пока код не станет удобным для поддержки.
Alvaro
По большому счету, ISettings — это именно что результат разбиения и структурирования. С некоторыми плюшками. И конечно же во многих проектах мой подход совершенно не нужен: как обычно, нужно выбирать инструмент по потребностям.