Итак, мы открыли для себя Dependency Injection, уяснили все его плюсы и несомненные пользы и начали вовсю применять его в своих проектах. Давайте посмотрим, что же еще можно делать при помощи Dependency Injection на примере библиотеки Ninject.
Для работоспособности кода нам понадобится, помимо непосредственно Ninject, установить еще три расширения: Ninject.Extensions.Factory, Ninject.Extensions.Interception и Ninject.Extensions.Interception.DynamicProxy. Эти расширения доступны в NuGet с соответствующими идентификаторами.
Рассмотрим довольно частую ситуацию. В проекте есть несколько репозиториев, инкапсулирующих в себе работу с базой данных. Пусть это будут UserRepository, CustomerRepository, OrderRepository. Помимо этого, в бизнес-слое есть класс Worker, который обращается к этим репозиториям. Мы желаем ослабить зависимости, выделяем из репозиториев интерфейсы и разрешаем зависимости через DI-контейнер:
Уже на этом этапе в голове начинает звенеть тревожный звоночек: а не слишком ли много зависимостей у нас внедряется в класс Worker? Что будет, если Worker'у придется обратиться к еще паре-тройке репозиториев? И постепенно начинает вырисовываться пока еще будущая проблема: «замусоривание» рабочих классов огромным количеством инъекций.
При этом мы замечаем, что наши репозитории относятся к одному слою, можно даже сказать — к одному «семейству» классов. (в зависимости от проекта возможно даже все репозитории наследуются от одного родительского класса). Это отличная возможность воспользоваться механизмом фабрик, который предоставляет Ninject.
Итак, создаем интерфейс фабрики:
и прописываем реализацию этого интерфейса в нашем NinjectModule:
Обратите внимание: класс, который реализует IRepositoryFactory, мы не создавали! Да нам он и не нужен — его создаст Ninject, руководствуясь следующей логикой: каждый метод нашего интерфейса должен возвращать новый объект указанного типа. Если этот тип возможно разрешить через указанные в NinjectModule зависимости, то он будет разрешен и создан.
Внедрение фабрики позволяет заменить несколько зависимостей на одну:
Здесь можно заметить еще один плюс от использования фабрик. При классическом разрешении зависимостей движок Dependency Injection обязан пройти по всему дереву зависимостей и создать все экземпляры всех классов, которые участвуют в зависимостях. Иными словами, если в приложении 200 классов используют DI, то при попытке получения экземпляра класса, который находится на вершине дерева зависимостей, будет создано 200 экземпляров остальных классов, даже если в текущем сценарии будет использовано 10. Фабрика же поддерживает ленивую загрузку, т.е. в приведенном выше примере будет создан экземпляр только CustomerRepository и только при вызове метода Test.
Помимо уменьшения числа зависимостей, фабрика позволяет удобно работать с параметрами конструкторов при инъекции через конструктор. Добавим в конструктор UserRepository параметр userName:
и модифицируем интерфейс фабрики:
Теперь при вызове репозитория мы можем легко передать параметр в конструктор:
Ninject позволяет внедрять не только инъекции в типы данных, но и добавлять дополнительный функционал в методы, т. е. вносить аспекты. Рассмотрим такой, опять-таки, довольно частый пример. Предположим, мы хотим включить автоматическое логгирование для некоторых наших методов. Создадим класс лога и выделим интерфейс:
Теперь укажем, как именно мы будем модифицировать необходимые методы. Для этого мы должны реализовать интерфейс IInterceptor:
Разумеется, это неполноценный лог, исключение тут, в нарушение всех канонов, не пробрасывается дальше по стеку, а банально «проглатывается». Но для иллюстрации подойдет.
Идея здесь в том, что непосредственный вызов метода происходит во время invocation.Processed. А значит, мы можем до и после вызова этого метода добавить любую функциональность. Что мы и делаем, обрамляя вызов метода в try/catch и занося исключение (буде оно случится) в некоторый лог.
Включить Intercept для нужного метода/методов можно несколькими способами, самый простой и элегантный из которых — пометить метод специальным атрибутом. Давайте создадим этот атрибут. Он должен наследоваться от InterceptAttribute и указывать, каким именно Intercept пользоваться
И наконец пометим нашим атрибутом нужный виртуальный метод. Естественно, если метод будет невиртуальным, никакого Interception не произойдет, т.к. Ninject использует банальный механизм наследования и создания proxy-класса с переопределенными методами:
В нашем примере исключение будет перехвачено и выведено на консоль. При этом, поскольку мы ввели класс логгера в наш Interception опять-таки через dependency injection, наш рабочий класс даже «не догадывается» о существовании каких-то логгеров и прочих вспомогательных инструментов. Всё, что выдает в нем внедрение аспекта — атрибут LogException.
При этом в нашем NinjectModule есть разрешение зависимостей только для ILogger, поскольку разрешение для ExceptionInterceptor мы опять-таки указали в LogExceptionAttribute:
Для работоспособности кода нам понадобится, помимо непосредственно 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>(); } }
