[Из песочницы] Advanced Dependency Injection на примере Ninject
Итак, мы открыли для себя Dependency Injection, уяснили все его плюсы и несомненные пользы и начали вовсю применять его в своих проектах. Давайте посмотрим, что же еще можно делать при помощи Dependency Injection на примере библиотеки Ninject.Для работоспособности кода нам понадобится, помимо непосредственно Ninject, установить еще три расширения: Ninject.Extensions.Factory, Ninject.Extensions.Interception и Ninject.Extensions.Interception.DynamicProxy. Эти расширения доступны в NuGet с соответствующими идентификаторами.
ФабрикиРассмотрим довольно частую ситуацию. В проекте есть несколько репозиториев, инкапсулирующих в себе работу с базой данных. Пусть это будут UserRepository, CustomerRepository, OrderRepository. Помимо этого, в бизнес-слое есть класс Worker, который обращается к этим репозиториям. Мы желаем ослабить зависимости, выделяем из репозиториев интерфейсы и разрешаем зависимости через DI-контейнер: public class Worker { public Worker (IUserRepository userRepository, ICustomerRepository customerRepository, IOrderRepository orderRepository) {
} } Уже на этом этапе в голове начинает звенеть тревожный звоночек:, а не слишком ли много зависимостей у нас внедряется в класс Worker? Что будет, если Worker’у придется обратиться к еще паре-тройке репозиториев? И постепенно начинает вырисовываться пока еще будущая проблема: «замусоривание» рабочих классов огромным количеством инъекций.При этом мы замечаем, что наши репозитории относятся к одному слою, можно даже сказать — к одному «семейству» классов. (в зависимости от проекта возможно даже все репозитории наследуются от одного родительского класса). Это отличная возможность воспользоваться механизмом фабрик, который предоставляет Ninject.
Итак, создаем интерфейс фабрики:
public interface IRepositoryFactory { IUserRepository CreateUserRepository (); ICustomerRepository CreateCustomerRepository (); IOrderRepository CreateOrderRepository (); } и прописываем реализацию этого интерфейса в нашем NinjectModule:
public class CommonModule: NinjectModule
{
public override void Load ()
{
Bind
Bind
Внедрение фабрики позволяет заменить несколько зависимостей на одну:
public class Worker { private readonly IRepositoryFactory _repositoryFactory;
public Worker (IRepositoryFactory repositoryFactory) { _repositoryFactory = repositoryFactory; }
public void Test () { var customerRepository = _repositoryFactory.CreateCustomerRepository (); } } Здесь можно заметить еще один плюс от использования фабрик. При классическом разрешении зависимостей движок Dependency Injection обязан пройти по всему дереву зависимостей и создать все экземпляры всех классов, которые участвуют в зависимостях. Иными словами, если в приложении 200 классов используют DI, то при попытке получения экземпляра класса, который находится на вершине дерева зависимостей, будет создано 200 экземпляров остальных классов, даже если в текущем сценарии будет использовано 10. Фабрика же поддерживает ленивую загрузку, т.е. в приведенном выше примере будет создан экземпляр только CustomerRepository и только при вызове метода Test.
Помимо уменьшения числа зависимостей, фабрика позволяет удобно работать с параметрами конструкторов при инъекции через конструктор. Добавим в конструктор UserRepository параметр userName:
public class UserRepository: IUserRepository { public UserRepository (string userName) { } } и модифицируем интерфейс фабрики:
public interface IRepositoryFactory { IUserRepository CreateUserRepository (string userName); ICustomerRepository CreateCustomerRepository (); IOrderRepository CreateOrderRepository (); } Теперь при вызове репозитория мы можем легко передать параметр в конструктор:
public class Worker { private readonly IRepositoryFactory _repositoryFactory;
public Worker (IRepositoryFactory repositoryFactory) { _repositoryFactory = repositoryFactory; }
public void TestUser () { var userRepository = _repositoryFactory.CreateUserRepository («testUser»); } } Аспекты Ninject позволяет внедрять не только инъекции в типы данных, но и добавлять дополнительный функционал в методы, т. е. вносить аспекты. Рассмотрим такой, опять-таки, довольно частый пример. Предположим, мы хотим включить автоматическое логгирование для некоторых наших методов. Создадим класс лога и выделим интерфейс: public interface ILogger { void Log (Exception ex); }
public class Logger: ILogger { public void Log (Exception ex) { Console.WriteLine (ex.Message); } } Теперь укажем, как именно мы будем модифицировать необходимые методы. Для этого мы должны реализовать интерфейс IInterceptor:
public class ExceptionInterceptor: IInterceptor { private readonly ILogger _logger;
public ExceptionInterceptor (ILogger logger) { _logger = logger; }
public void Intercept (IInvocation invocation) { try { invocation.Proceed (); } catch (Exception ex) { _logger.Log (ex); } } } Разумеется, это неполноценный лог, исключение тут, в нарушение всех канонов, не пробрасывается дальше по стеку, а банально «проглатывается». Но для иллюстрации подойдет.
Идея здесь в том, что непосредственный вызов метода происходит во время invocation.Processed. А значит, мы можем до и после вызова этого метода добавить любую функциональность. Что мы и делаем, обрамляя вызов метода в try/catch и занося исключение (буде оно случится) в некоторый лог.
Включить Intercept для нужного метода/методов можно несколькими способами, самый простой и элегантный из которых — пометить метод специальным атрибутом. Давайте создадим этот атрибут. Он должен наследоваться от InterceptAttribute и указывать, каким именно Intercept пользоваться
public class LogExceptionAttribute: InterceptAttribute
{
public override IInterceptor CreateInterceptor (IProxyRequest request)
{
return request.Context.Kernel.Get
public class Worker { [LogException] public virtual void Test () { throw new Exception («test exception»); } } В нашем примере исключение будет перехвачено и выведено на консоль. При этом, поскольку мы ввели класс логгера в наш Interception опять-таки через dependency injection, наш рабочий класс даже «не догадывается» о существовании каких-то логгеров и прочих вспомогательных инструментов. Всё, что выдает в нем внедрение аспекта — атрибут LogException.При этом в нашем NinjectModule есть разрешение зависимостей только для ILogger, поскольку разрешение для ExceptionInterceptor мы опять-таки указали в LogExceptionAttribute:
public class CommonModule: NinjectModule
{
public override void Load ()
{
Bind