top of page

Dependency Injection (DI) и Inversion of Control (IoC)

Dependency Injection (DI) и Inversion of Control (IoC) являются популярными концепциями организации кода, реализация DI и IoC подходов при разработке программного обеспечения делает код более гибким, расширяемым и тестируемым.


Суть Dependency Injection заключается в том, что класс не создает необходимые зависимости самостоятельно, а получает их извне. Ответственность за создание и внедрение зависимостей переходит (Inversion of Control) от класса к специальному компоненту.


Давайте рассмотрим, что такое DI и IoC, и зачем они нужны.


Представьте, у вас в программе есть class A. Допустим, для выполнения своих функций, классу A нужен объект класса B. Например, класс A - автомобиль (Auto), a класс B - двигатель (Engine). Автомобилю нужен двигатель. Или, например, класс A - сервис заказов (OrderService), класс B - хранилище заказов (OrderRepository). Сервису заказов нужно хранилище заказов. Или, может быть, класс A - Messenger, а класс B - WiFiConnection. Мессенджеру для отправки сообщений нужен WiFiConnection.


Во всех этих примерах мы можем говорить, что класс A зависит от класса B (Auto зависит от Engine, Messenger зависит от WiFiConnection, OrderService зависит от OrderRepository).


Зависимость - это связь между двумя или более компонентами программного обеспечения, где один компонент (называемый зависимым) использует функциональность или ресурсы другого компонента (называемого зависимостью). Зависимости могут быть классами, интерфейсами, модулями, библиотеками и т. д.

В коде это может быть отражено, например, следующим образом:


public class Auto {
   Engine engine = new Engine();
  
   public void go(){
       engine.startEngine();
   }
}

Т.е. класс A (Auto) непосредственно сам создает зависимость - объект класса B(Engine). Подумайте, какие недостатки есть у такой реализации?


  1. класс A жестко связан с классом B. Если возникнет необходимость реализовать функции класса B иначе (например, сделать автомобиль с другим двигателем) тогда нам придется внести изменения и в класс A

  2. у нас нет возможности проверить класс A, в отрыве от класса B. Т.е. например, если в классе WiFiConnection у нас ошибка, или нет подключения, у нас нет возможности проверить Messenger

  3. Это решение плохо масштабируется. Например, если OrderRepository не справляется с нагрузкой, у нас нет возможности перенаправить заказы в другой репозиторий.

  4. Часть кода класса А, причем это может быть достаточно существенная часть кода, выполняет не основную задачу класса, а занимается созданием объектов-зависимостей.

Данный подход к реализации характеризуется высокой связанностью (Coupling) т.е. большой степенью зависимости классов друг от друга.

Давайте попробуем уменьшить степень связанности наших классов применив подход Dependency Injection (DI) и Inversion of Control (IoC), для этого выполним два простых шага:

  1. Сделаем класс B интерфейсом. Теперь его сможет заменить любой класс реализующий интерфейс. Например, у машины может быть бензиновая или дизельная реализация двигателя, Messenger может пользоваться WiFiConnection или GSM и т.д.

  2. Пусть класс A не создает реализацию интерфейса B самостоятельно. Пусть теперь он получает этот объект как параметр в конструкторе или как параметр в сеттере.

Таким образом, мы получили возможность отделить объект A и объект B (точнее, объект реализующий interface B).


В коде этот переход будет выглядеть примерно так:

В классе A (Auto) делаем конструктор, куда в качестве параметра приходит любой объект реализующий interface Engine.



public class Auto {
   private Engine engine;


   public Auto(Engine engine) {
       this.engine = engine;
   }


   public void go(){
       engine.startEngine();
   }
}



public interface Engine {
   public void startEngine();


}

Теперь создание машины будет выглядеть так:

Engine electricEngine = new ElectricEngineImplementation();
Auto auto = new Auto(electricEngine);

Т.е. теперь зависимость (в нашем примере ElectricEngineImplementation)не создается непосредственно в классе A (Auto) а внедряется (inject) в объект класса A извне, через конструктор. По сути, мы передали контроль (inversion of control) за создание зависимости для класса A во внешний компонент. Это дало нам следующие преимущества:


  1. Упрощение тестирования: Зависимости могут быть заменены на фиктивные или моки во время тестирования, что упрощает создание изолированных тестовых сред.

  2. Гибкость: Зависимости могут быть легко заменены или модифицированы без изменения кода класса, который их использует. Мы можем внедрить любой двигатель без модификации автомобиля.

  3. Разделение ответственности: Классы стали более специализированными, так как они не заботятся о создании и управлении своими зависимостями.

  4. Повторное использование кода: Зависимости могут быть использованы в нескольких классах без дублирования кода.

На практике польза от такого подхода становится тем более очевидной, чем больше зависимостей есть в проекте. Представьте, что для создания автомобиля нужно создать не только двигатель, но и кузов, салон, шасси, трансмиссию и т.д. и у каждого компонента, в свою очередь, есть свои зависимости.

Для реализации DI и IoC в Java используются различные фреймворки, такие как Spring или Google Guice. Таким образом, на практике именно фреймворк выступает тем внешнем компонентом, который создает и внедряет зависимости, реализуя Dependency Injection (DI) и Inversion of Control (IoC).





316 просмотров

Недавние посты

Смотреть все
bottom of page