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). Подумайте, какие недостатки есть у такой реализации?
класс A жестко связан с классом B. Если возникнет необходимость реализовать функции класса B иначе (например, сделать автомобиль с другим двигателем) тогда нам придется внести изменения и в класс A
у нас нет возможности проверить класс A, в отрыве от класса B. Т.е. например, если в классе WiFiConnection у нас ошибка, или нет подключения, у нас нет возможности проверить Messenger
Это решение плохо масштабируется. Например, если OrderRepository не справляется с нагрузкой, у нас нет возможности перенаправить заказы в другой репозиторий.
Часть кода класса А, причем это может быть достаточно существенная часть кода, выполняет не основную задачу класса, а занимается созданием объектов-зависимостей.
Данный подход к реализации характеризуется высокой связанностью (Coupling) т.е. большой степенью зависимости классов друг от друга.
Давайте попробуем уменьшить степень связанности наших классов применив подход Dependency Injection (DI) и Inversion of Control (IoC), для этого выполним два простых шага:
Сделаем класс B интерфейсом. Теперь его сможет заменить любой класс реализующий интерфейс. Например, у машины может быть бензиновая или дизельная реализация двигателя, Messenger может пользоваться WiFiConnection или GSM и т.д.
Пусть класс 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 во внешний компонент. Это дало нам следующие преимущества:
Упрощение тестирования: Зависимости могут быть заменены на фиктивные или моки во время тестирования, что упрощает создание изолированных тестовых сред.
Гибкость: Зависимости могут быть легко заменены или модифицированы без изменения кода класса, который их использует. Мы можем внедрить любой двигатель без модификации автомобиля.
Разделение ответственности: Классы стали более специализированными, так как они не заботятся о создании и управлении своими зависимостями.
Повторное использование кода: Зависимости могут быть использованы в нескольких классах без дублирования кода.
На практике польза от такого подхода становится тем более очевидной, чем больше зависимостей есть в проекте. Представьте, что для создания автомобиля нужно создать не только двигатель, но и кузов, салон, шасси, трансмиссию и т.д. и у каждого компонента, в свою очередь, есть свои зависимости.
Для реализации DI и IoC в Java используются различные фреймворки, такие как Spring или Google Guice. Таким образом, на практике именно фреймворк выступает тем внешнем компонентом, который создает и внедряет зависимости, реализуя Dependency Injection (DI) и Inversion of Control (IoC).
留言