Повышение качества кода и удобство сопровождения с помощью внедрения зависимостей
Внедрение зависимостей (DI) — это шаблон проектирования и метод программирования для управления зависимостями между различными компонентами.
В DI зависимости класса или другого зависимого компонента создаются и предоставляются извне (внедряются), а не начинают создаваться зависимым компонентом.
Понимание внедрения зависимостей является ключом к следованию принципу инверсии зависимостей.
Основные компоненты
Три основных компонента внедрения зависимостей:
- Зависимость. Зависимость – это объект или служба, на которые другой класс опирается при выполнении своих задач. Он представляет собой контракт или интерфейс, определяющий требуемую функциональность.
- Клиент: зависимый класс, также известный как клиентский класс, — это класс, который полагается на зависимость для выполнения своих функций. Обычно он объявляет зависимость через параметры конструктора, методы установки или контракты интерфейса.
- Инжектор. Инжектор (он же контейнер, ассемблер, фабрика) отвечает за создание и управление зависимостями, а также за их внедрение в зависимый класс (клиент). Инжектор может быть фреймворком или контейнером, предоставляемым DI-библиотекой, или пользовательской реализацией.
Эти три компонента работают вместе, чтобы обеспечить преимущества DI, такие как слабая связь, модульность, тестируемость и гибкость.
Зависимый класс полагается на инжектор или контейнер для предоставления необходимых зависимостей, а сами зависимости определяются как контракты или интерфейсы, что позволяет использовать взаимозаменяемые реализации. Такое разделение задач и инверсия управления приводит к более легкому сопровождению и масштабируемости кода.
Выполнение
В качестве примера рассмотрим два класса: Engine и Car.
Чтобы создать экземпляр класса Car, нам нужен соответствующий объект Engine.
class Engine {
private horsepower: number;
private fuelType: string;
constructor(horsepower: number, fuelType: string) {
this.horsepower = horsepower;
this.fuelType = fuelType;
}
public start() {
console.log(`Engine started. Horsepower: ${this.horsepower}, Fuel Type: ${this.fuelType}`);
}
public stop() {
console.log("Engine stopped.");
}
}
class Car {
private engine: Engine;
private brand: string;
private model: string;
constructor(brand: string, model: string) {
this.brand = brand;
this.model = model;
this.engine = new Engine(200, "Gasoline");
}
public startCar() {
console.log(`Starting ${this.brand} ${this.model}`);
this.engine.start();
}
public stopCar() {
console.log(`Stopping ${this.brand} ${this.model}`);
this.engine.stop();
}
}
// Example usage
const car = new Car("Toyota", "Camry");
car.startCar();
car.stopCar();
// To consturct a car with Gasoline engine required a manual edit of Car class
На данный момент это работает нормально, за исключением того, что если нам нужно передать другие параметры в класс Engine, требуется ручное редактирование.

Ввод параметров
Чтобы решить такую проблему, мы можем воспользоваться инъекцией параметров. Перепишем текущий код.
class Engine {
// same implementation
}
class Car {
private engine: Engine;
private brand: string;
private model: string;
constructor(brand: string, model: string, horsepower: number, fuelType: string) {
this.brand = brand;
this.model = model;
this.engine = new Engine(horsepower, fuelType);
}
public startCar() {
console.log(`Starting ${this.brand} ${this.model}`);
this.engine.start();
}
public stopCar() {
console.log(`Stopping ${this.brand} ${this.model}`);
this.engine.stop();
}
}
// Example usage
const car1 = new Car("Toyota", "Camry", 200, "Gasoline");
car1.startCar();
car1.stopCar();
// Easy change Engine parameters
const car2 = new Car("BMW", "X5", 300, "Diesel");
car2.startCar();
car2.stopCar();
Теперь общая логика не меняется; вместо этого мы можем легко вносить изменения в соответствии с нашими потребностями.

Внедрение конструктора/установщика
В предыдущем примере мы использовали внедрение параметров, чтобы изменить horsepower и fuelType для класса Engine. Однако это может стать громоздким при работе с большим количеством зависимостей.
Чтобы сделать эти 2 класса более гибкими для изменения и тестирования, принято создавать необходимую зависимость вне зависимого класса. Вы можете достичь этого результата, используя инъекцию конструктора или сеттера.
class Engine {
// same implementation
}
class Car {
private engine: Engine;
private brand: string;
private model: string;
constructor(brand: string, model: string, engine: Engine) {
this.brand = brand;
this.model = model;
this.engine = engine;
}
public startCar() {
// same logic
}
public stopCar() {
// same logic
}
}
// Example usage
const gasolineEngine = new Engine(200, "Gasoline");
const car1 = new Car("Toyota", "Camry", gasolineEngine);
car1.startCar();
car1.stopCar();
// Easy change Engine parameters
const dieselEngine = new Engine(300, "Diesel");
const car2 = new Car("BMW", "X5", dieselEngine);
car2.startCar();
car2.stopCar();
Снимая ответственность за создание экземпляра двигателя с класса Car, вы придерживаетесь принципа единой ответственности. Класс Car должен сосредоточиться на своих собственных обязанностях, связанных с поведением автомобиля, в то время как создание и настройка двигателя может обрабатываться в другой части кода.
Та же реализация, но с внедрением сеттера:
class Engine {
// same implementation
}
class Car {
private brand: string;
private model: string;
private engine: Engine;
constructor(brand: string, model: string) {
this.brand = brand;
this.model = model;
}
public setEngine(engine: Engine) {
this.engine = engine;
}
public startCar() {
// same logic
}
public stopCar() {
// same logic
}
}
// Example usage
const gasolineEngine = new Engine(200, "Gasoline");
const car1 = new Car("Toyota", "Camry");
car1.setEngine(gasolineEngine);
car1.startCar();
car1.stopCar();
const dieselEngine = new Engine(300, "Diesel");
const car2 = new Car("BMW", "X5");
car2.setEngine(dieselEngine);
car2.startCar();
car2.stopCar();

Инъекция интерфейса
Прямо сейчас текущая реализация Car привязана к определенному классу Engine. Это может быть проблемой, если отдельные экземпляры класса Engine требуют другой логики.
Чтобы сделать классы Engine и Car более слабо связанными, мы можем привязать Car к интерфейсу (или абстрактному классу в качестве интерфейса) вместо конкретного дочернего класса Engine.
interface Engine {
start(): void;
stop(): void;
}
class GasolineEngine implements Engine {
private horsepower: number;
private fuelType: string;
constructor(horsepower: number) {
this.horsepower = horsepower;
this.fuelType = "Gasoline";
}
public start() {
console.log(`Gasoline engine started. Horsepower: ${this.horsepower}`);
}
public stop() {
console.log("Gasoline engine stopped.");
}
}
class DieselEngine implements Engine {
private horsepower: number;
private fuelType: string;
constructor(horsepower: number) {
this.horsepower = horsepower;
this.fuelType = "Diesel";
}
public start() {
console.log(`Diesel engine started. Horsepower: ${this.horsepower}`);
}
public stop() {
console.log("Diesel engine stopped.");
}
}
class Car {
private engine: Engine;
private brand: string;
private model: string;
// class Car expect any valid Engine implementation
constructor(brand: string, model: string, engine: Engine) {
this.brand = brand;
this.model = model;
this.engine = engine;
}
public startCar() {
// same logic
}
public stopCar() {
// same logic
}
}
// Example usage
const gasolineEngine = new GasolineEngine(200);
const car1 = new Car("Toyota", "Camry", gasolineEngine);
car1.startCar();
car1.stopCar();
const dieselEngine = new DieselEngine(300);
const car2 = new Car("BMW", "X5", dieselEngine);
car2.startCar();
car2.stopCar();
Теперь класс Car отделен от конкретной реализации класса Engine. Это позволяет вам легко заменять различные типы двигателей без изменения самого класса Car.

Форсунки
До сих пор я говорил только о зависимостях и клиентах.
Ручное создание зависимостей может быть болезненным. Особенно, если есть несколько уровней вложенности. Тут на помощь приходят форсунки.
Инжектор разрешает зависимости и предоставляет их клиентскому классу. Вы можете создать собственный алгоритм регистрации и внедрения зависимостей, а можете использовать DI-контейнеры или DI-фреймворки, которые сделают это за вас.
Примерами для JavaSript/TypeScript являются InversifyJS, Awilix, TypeDI и NestJS, для C# — ASP.NET Core Dependency Injection, для Java — Spring Framework и для Go — Google Wire.
Перепишем последнюю реализацию с внедрением интерфейса с помощью контейнера TypeDI:
import { Service, Inject, Container } from 'typedi';
import 'reflect-metadata';
interface Engine {
start(): void;
stop(): void;
}
@Service()
class GasolineEngine implements Engine {
private horsepower: number;
private fuelType: string;
constructor(@Inject('horsepower') horsepower: number) {
this.horsepower = horsepower;
this.fuelType = 'Gasoline';
}
start() {
console.log(`Gasoline engine started. Horsepower: ${this.horsepower}`);
}
stop() {
console.log('Gasoline engine stopped.');
}
}
@Service()
class DieselEngine implements Engine {
private horsepower: number;
private fuelType: string;
constructor(@Inject('horsepower') horsepower: number) {
this.horsepower = horsepower;
this.fuelType = 'Diesel';
}
start() {
console.log(`Diesel engine started. Horsepower: ${this.horsepower}`);
}
stop() {
console.log('Diesel engine stopped.');
}
}
@Service()
class Car {
private engine: Engine;
private brand: string;
private model: string;
constructor(@Inject('brand') brand: string, @Inject('model') model: string, @Inject('engine') engine: Engine) {
this.brand = brand;
this.model = model;
this.engine = engine;
}
public startCar() {
console.log(`Starting ${this.brand} ${this.model}`);
this.engine.start();
}
public stopCar() {
console.log(`Stopping ${this.brand} ${this.model}`);
this.engine.stop();
}
}
// Register dependencies with the container
Container.set('horsepower', 200);
Container.set('brand', 'Toyota');
Container.set('model', 'Camry');
Container.set({ id: 'engine', type: GasolineEngine });
Container.set({ id: Car, type: Car });
Container.set('horsepower', 300);
Container.set('brand', 'BMW');
Container.set('model', 'X5');
Container.set({ id: 'engine', type: DieselEngine });
Container.set({ id: Car, type: Car });
// Example usage
const car1 = Container.get(Car);
car1.startCar();
car1.stopCar();
const car2 = Container.get(Car);
car2.startCar();
car2.stopCar();
// console.log:
Starting Toyota Camry
Gasoline engine started. Horsepower: 200
Stopping Toyota Camry
Gasoline engine stopped.
Starting BMW X5
Diesel engine started. Horsepower: 300
Stopping BMW X5
Diesel engine stopped.
Использование контейнера внедрения зависимостей упрощает управление зависимостями и клиентами. Это не только позволяет создавать сложный граф зависимостей, но и упрощает тестирование компонентов с помощью заглушек и макетов.
Заключение
Таким образом, внедрение зависимостей — ценный метод проектирования гибких, модульных и тестируемых программных систем. Это способствует слабой связи, повышает возможность повторного использования кода и упрощает настройку и управление зависимостями.
Применяя DI, вы можете писать более удобные в сопровождении, масштабируемые и надежные приложения.
Использованная литература:
- Википедия: Инъекция зависимостей
- Мартин Фаулер: Инверсия контейнеров управления и шаблон внедрения зависимостей
Спасибо, что прочитали эту статью! Если у вас есть какие-либо вопросы или предложения, не стесняйтесь писать комментарии.
Посмотрите другие мои статьи:
Дополнительные материалы на PlainEnglish.io.
Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .