Итак, мы открыли для себя 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<IUserRepository>().To<UserRepository>();
Bind<ICustomerRepository>().To<CustomerRepository>();
Bind<IOrderRepository>().To<OrderRepository>();
Bind<IRepositoryFactory>().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<ExceptionInterceptor>();
}
}
И наконец пометим нашим атрибутом нужный виртуальный метод. Естественно, если метод будет невиртуальным, никакого 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<ILogger>().To<Logger>();
}
}
Автор: Defazze7