Впровадження залежностей

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

Впровадження залежності (англ. Dependency injection, DI) — шаблон проєктування програмного забезпечення, що передбачає надання зовнішньої залежності програмному компоненту, використовуючи «інверсію керування» (англ. Inversion of control, IoC) для розв'язання (отримання) залежностей.

Впровадження — це передача залежності (тобто, сервісу) залежному об'єкту (тобто, клієнту). Передавати залежності клієнту замість дозволити клієнту створити сервіс є фундаментальною вимогою до цього шаблону проєктування.

Існує три найпоширеніші форми впровадження залежностей:

Огляд[ред. | ред. код]

Впровадження залежностей — це шаблон проєктування, в якому залежності (або сервіси) впроваджуються, або передаються по посиланню в залежний об'єкт (клієнт) і стають частиною клієнтського стану. Шаблон відокремлює створення залежностей клієнта від власної логіки клієнта, що дозволяє компонентам бути слабко зв'язаними і притримуватися принципів інверсії залежностей і єдиного обов'язку. Це суперечить анти-шаблону service locator, який дозволяє клієнтам знати про систему, що використовується для пошуку залежностей.

Переваги[ред. | ред. код]

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

Структура[ред. | ред. код]

UML класи та діаграма послідовності[ред. | ред. код]

Приклад UML класу та діаграми послідовності шаблону проєктування Впровадження Залежностей

В наведеній вище UML діаграмі класів, клас Client, який потребує об'єкти ServiceA та ServiceB не інстанціює класи ServiceA1 та ServiceB1 напряму. Замість цього клас Injector створює об'єкти та впроваджує їх в Client, що робить Client незалежним від того як, ці об'єкти створюються (який конкретно клас інстанціюється).

UML діаграма послідовності демонструє взаємодії часу виконання: Об'єкт Injector створює об'єкти ServiceA1 та ServiceB1. Після цього Injector створює об'єкт Client і впроваджує в нього об'єкти ServiceA1 та ServiceB1.

Приклади[ред. | ред. код]

Без впровадження залежностей[ред. | ред. код]

В наступному C# прикладі, клас Client містить в собі поле класу Service, яке ініціалізується в конструкторі класу Client. Клієнт має контроль над тим, яка реалізація сервіса використовується, оскільки сам створює її. В цьому прикладі клієнт має жорстку залежність від ServiceExample().

// An example without dependency injection
public class Client 
{
    // Internal reference to the service used by this client
    private readonly Service _service;

    // Constructor
    public Client() 
    {
        // Specify a specific implementation in the constructor instead of using dependency injection
        _service = new ServiceExample();
    }

    // Method within this client that uses the services
    public string Greet() 
    {
        return "Hello " + _service.Name;
    }
}

Впровадження через конструктор[ред. | ред. код]

Клас, якому потрібна залежність, повинен надати відкритий конструктор, який приймає екземпляр необхідної залежності як аргумент конструктора. У більшості випадків, це повинен бути тільки public конструктор. Якщо необхідна більш ніж одна залежність, можуть бути використані додаткові аргументи конструктора. Є найбільш широко вживаним і рекомендованим методом впровадження залежностей.

public class Client 
{
    private readonly IService _service;
 
    // Constructor
    public Client(IService service) 
   {
        if (service == null)
        {
          throw new ArgumentNullException("service");
        }
        _service = service;
    }
 
    // Method within this client that uses the services
    public string Greet() 
    {
        return "Hello " + _service.Name;
    }
}

Впровадження через властивість[ред. | ред. код]

Клас, який використовує залежність, повинен надати відкриту, доступну для запису властивість типу залежності. Client залежить від IService. Клієнти можуть поставляти реалізації IService, встановлюючи властивість Dependency. На відміну від впровадження в конструктор, ви не можете відзначити поле властивості Dependency як readonly, тому що ви дозволяєте викликаючим елементам змінювати цю властивість в будь-який момент життєвого циклу Client. Однак така реалізація є крихкою, тому що немає гарантії, що властивість Dependency повертає екземпляр IService. Код, як цей, викине NullReferenceException, якщо значення властивості Dependency — null.

public class Client 
{
    public IService Dependency {get; set;}
  
    // Method within this client that uses the services
    public string Greet() 
    {
        return "Hello " + Dependency.Name;
    }
}

Впровадження через метод[ред. | ред. код]

Елемент, що викликає метод, впроваджує залежність як параметр методу в кожен виклик методу. Впровадження в метод краще використовувати тоді, коли залежність може змінюватися з кожним викликом методу. Це може бути в тому випадку, коли залежність сама по собі представляє значення, або коли елемент, що викликає, надає споживачеві інформацію про контекст, в якому викликається операція.

public class Client 
{
    // Method within this client that uses the services
    public string Greet(IService service) 
    {
        if (service == null)
        {
          throw new ArgumentNullException("service");
        }
        return "Hello " + service.Name;
    }
}

Конфігурування через XML[ред. | ред. код]

Конфігурування DI-контейнеру Unity за допомогою XML

 <register type="IBasketService" mapTo="BasketService" />
 <register type="BasketDiscountPolicy" mapTo="RepositoryBasketDiscountPolicy" />
 <register type="BasketRepository" mapTo="SqlBasketRepository">
   <constructor>
     <param name="connString">
       <value value="CommerceObjectContext" typeConverter="ConnectionStringConverter" />
     </param>
   </constructor>
 </register>
 <register type="DiscountRepository" mapTo="SqlDiscountRepository">
   <constructor>
     <param name="connString">
       <value value="CommerceObjectContext" typeConverter="ConnectionStringConverter" />
     </param>
   </constructor>
 </register>
 <register type="ProductRepository" mapTo="SqlProductRepository">
   <constructor>
     <param name="connString">
       <value value="CommerceObjectContext" typeConverter="ConnectionStringConverter" />
     </param>
   </constructor>
 </register>
 <register type="CurrencyProvider" mapTo="SqlCurrencyProvider">
   <constructor>
     <param name="connString">
       <value value="CommerceObjectContext" typeConverter="ConnectionStringConverter" />
     </param>
   </constructor>
 </register>

Перетворення інтерфейсу IBasketService в клас BasketService реалізується за допомогою простого елемента register. Деякі конкретні класи приймають рядок з'єднання як вхідні дані, тому необхідно визначити, яким чином знаходиться значення цього рядка. Що стосується Unity, можна зробити це, вказавши, що ви використовуєте користувацький тип конвертера під назвою ConnectionStringConverter. Цей конвертер буде шукати значення CommerceObjectContext серед стандартних рядків з'єднання web.config і повертати рядок з'єднання з цим ім'ям. Решта елементів повторюють ці два патерни. Оскільки Unity може автоматично перетворювати запити в конкретні типи, навіть якщо відсутні явні реєстрації, вам не потрібно застосовувати XML-елементи для HomeController і BasketController. Завантаження конфігурації в контейнер виконується за допомогою виклику єдиного методу: container.LoadConfiguration();

Попередження[ред. | ред. код]

Як тільки ваш застосунок(ісп.) буде виростати в розмірах і ускладнюватися, теж саме буде відбуватися і з вашим конфігураційним файлом, якщо ви використовуєте конфігураційну композицію. Він може стати справжньою проблемою, оскільки цей файл моделює такі сутності коду, як класи, параметри тощо, але без переваг компілятора, опцій налагодження і т. д. Файли будуть ставати крихкими і непрозорими з точки зору наявності помилок, тому використовуйте даний підхід тільки, якщо вам необхідно пізнє зв'язування.

Конфігурування програми за допомогою коду[ред. | ред. код]

 c.For<IBasketService>().Use<BasketService>();
 c.For<BasketDiscountPolicy>().Use<RepositoryBasketDiscountPolicy>();
 string connectionString = ConfigurationManager.ConnectionStrings["CommerceObjectContext"].ConnectionString;
 c.For<BasketRepository>().Use<SqlBasketRepository>().Ctor<string>().Is(connectionString);
 c.For<DiscountRepository>().Use<SqlDiscountRepository>().Ctor<string>().Is(connectionString);
 c.For<ProductRepository>().Use<SqlProductRepository>().Ctor<string>().Is(connectionString);
 c.For<CurrencyProvider>().Use<SqlCurrencyProvider>().Ctor<string>().Is(connectionString);

Для того щоб підтримати ті класи, для яких потрібен рядок з'єднання, ви продовжуєте послідовність For/Use шляхом виклику методу Ctor та передачі рядка з'єднання. Метод Ctor виконує пошук строкового параметра в конструкторі конкретного класу і використовує передане значення для цього параметра.

Використання коду як конфігурації не тільки компактніше XML-конфігурації, але також підтримується компілятором. Типи аргументів являють собою реальні типи, які перевіряє компілятор. Змінна API StructureMap поставляється навіть з деякими видовими обмежувачами, які повідомляють компілятору про перевірку того, чи збігається тип, який визначається методом використання з абстракціями, позначеними за допомогою методу For. Якщо перетворення неможливо, то код не компілюється.

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

Див. також[ред. | ред. код]

Посилання[ред. | ред. код]