[Из песочницы] 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().To(); Bind().To(); Bind().To();

Bind().ToFactory (); } } Обратите внимание: класс, который реализует IRepositoryFactory, мы не создавали! Да нам он и не нужен — его создаст Ninject, руководствуясь следующей логикой: каждый метод нашего интерфейса должен возвращать новый объект указанного типа. Если этот тип возможно разрешить через указанные в NinjectModule зависимости, то он будет разрешен и создан.

Внедрение фабрики позволяет заменить несколько зависимостей на одну:

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(); } } И наконец пометим нашим атрибутом нужный виртуальный метод. Естественно, если метод будет невиртуальным, никакого Interception не произойдет, т.к. Ninject использует банальный механизм наследования и создания proxy-класса с переопределенными методами:

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().To(); } }

© Habrahabr.ru