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


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.