Monday, 29 July 2013

Dependency Injection та Inversion of Control контейнер

Одним з найпоширеніших методів зменешення звязаності компонентів алпікації є заміна обєктів конкретного типу на інтерфейси, що сьогодні я постараюсь коротенько описати.

Dependency Injection

Отож наша тестова аплікація умовно працюватиме з базою даних, відповідно маємо клас DbRepository який начебто реалізує роботу з базою даних та клас WithoutDI який як понятно з назви не використовує фічу про яку я сьогодні пишу, а умовно містить в собі певну логіку яка виконується перед тим як працювати з даними збереженими в БД.

На прикладі ци виглядатиме наступним чином:

   1: /// <summary>
   2: /// Class without Dependency Injection
   3: /// </summary>
   4: class WithoutDI
   5: {
   6:     DbRepository _repository = new DbRepository();
   7:  
   8:     public void UseRepo()
   9:     {
  10:         // Dummy logic.
  11:  
  12:         Console.WriteLine("I am " + this.GetType().Name + " and I work with " + _repository.GetType().Name);
  13:     }
  14: }

Як видно з коду наш клас має жорстку привязку до класу DbRepository і відповідно, якщо ми захочемо замінити репозиторій на інший то нам також придеться зайти в клас WithoutDI і змінити назву класу(DbRepository ) на нову. Ще одна проблема, яка не одразу кидається в очі, це те, що відтестуватитакий клас складно так як він в теорії працює з реальною базою даних і відповідно виклик любого з методів буде мати вплив на дані в базі. Отож ми трохи змінюємо код і в результаті отримуємо щось типу:

   1: /// <summary>
   2: /// Class with Dependency Injection
   3: /// </summary>
   4: class WithDI
   5: {
   6:     private IRepository _repository;
   7:  
   8:     public WithDI(IRepository repository)
   9:     {
  10:         _repository = repository;
  11:     }
  12:  
  13:     public void UseRepo()
  14:     {
  15:         // Dummy logic.
  16:  
  17:         Console.WriteLine("I am " + this.GetType().Name + " and I work with " + _repository.GetType().Name);
  18:     }
  19: }

В даному випадку ми приймаємо репозиторій з яким ми будемо працювати в конструкторі, при цьому не конкретний а інтерфейс, що в свою чергу дозволяє передати туди любий обєкт який наслідує цей інтерфейс. Наприклад якщо в нас є декілька реалізацій репозиторіїв то ми можемо використати їх наступним чином:

   1: WithDI withDbDi = new WithDI(new DbRepository());
   2: WithDI withFileDi = new WithDI(new FileRepository());
   3:  
   4: withDbDi.UseRepo();
   5: withFileDi.UseRepo();

В першому випадку ми будемо працювати з репозиторієм бази даних, а в другому з репозиторієм який зберігає дані в файл.

Inversion of Control

IOC контейнер це абстракція яка виносить реєстрацію всіх залежностей в одне місце і автоматично створює необхідні обєкти там де вони потрібні. Опис в мене получився не дуже зрозумілий так що краще розглянемо на прикладі. Я створив простенький клас основною метою якого є реєстрація та повернення створених обєктів. Ось його код:

   1: /// <summary>
   2: /// My as simple as possible IOC 
   3: /// </summary>
   4: class IOC
   5: {
   6:     Dictionary<string, object> dependencies = new Dictionary<string, object>();
   7:  
   8:     public void Register(string name, object obj)
   9:     {
  10:         dependencies.Add(name, obj);
  11:     }
  12:  
  13:     public object Resolve(string name)
  14:     {
  15:         return dependencies[name];
  16:     }
  17: }

Код не містить жодних перевірок і тд для того щоб була зрозуміла основна ідея.

Використати його дуже просто

   1: IOC ioc = new IOC();
   2:  
   3: ioc.Register("someRepo", new DbRepository());
   4: ioc.Register("OtherRepo", new FileRepository());

В місці де нам потрібний обєкт просто викликаємо метод Resolve з іменем обєкту який ми хочемо отримати.

   1: /// <summary>
   2: /// Class with Dependency Injection
   3: /// </summary>
   4: class WithIOC
   5: {
   6:     private IRepository _repository;
   7:  
   8:     public WithIOC(IOC ioc)
   9:     {
  10:         _repository = (IRepository)ioc.Resolve("someRepo");
  11:     }
  12:  
  13:     public void UseRepo()
  14:     {
  15:         // Dummy logic.
  16:  
  17:         Console.WriteLine("I am " + this.GetType().Name + " and I work with " + _repository.GetType().Name + "recieved from IOC");
  18:     }
  19: }

Таким чином в нас для того щоб змінити один репозиторій на інший треба просто зарєеструвати в одному місці той обєкт якй нам потрібно і відповідно всюди по коду де витягується обєкт за цим іменем буде повернено інший обєкт.

Тобто в нас в коді є одне місце в якому можна керувати тим яким репозиторієм ми будемо користуватись і в випадку коли ми тестуємо код або змінюємо тип репозиторія ми можемо зробити це в одному місці, простою зміною обєкту який пхається в ІОС. Спільне використання цих технік сприєя не лише кращому тестуванню, але й має багато інших позитивних моментів Улыбка Для кращого розуміння вартує перечитати ще декілька статей на тему, бо головною метою для себе поставив лише короткий опис, а сама тема досить фундаментальна і її знання обовязкове.

Стаття Мартіна Фовлера: http://martinfowler.com/articles/injection.html

Код: https://github.com/BatsIhor/DI_and_IOC_demo/