Advanced Dependency Injection на примере Ninject

в 8:16, , рубрики: .net, dependecy 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<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

Источник

* - обязательные к заполнению поля