Dependency Injection (DI / Внедрение зависимостей) — это стиль написания кода (паттерн), при котором класс не создает нужные ему для работы объекты сам, а получает их извне (обычно через конструктор).
🧩 Как DI связано с принципами SOLID?
DI напрямую связано с двумя принципами SOLID, которые мы разбирали ранее:
- D — Dependency Inversion Principle (Принцип инверсии зависимостей): DIP — это архитектурное правило (теория), которое говорит: «Завись от интерфейсов, а не от реализаций». А Dependency Injection — это инструмент (практика), с помощью которого мы передаем эту конкретную реализацию интерфейса внутрь класса.
- S — Single Responsibility Principle (Принцип единственности ответственности):
Если класс сам внутри себя создает объекты через
new, он берет на себя лишнюю ответственность — создание и настройку чужих объектов. С использованием DI класс занимается только своей бизнес-логикой.
❌ Без Dependency Injection (Плохой код)
Класс Car (Автомобиль) сам создает внутри себя объект Engine (Двигатель).
Класс жестко связан с конкретным двигателем (Бензиновым). Мы не можем заменить мотор, не переписывая код машины.
class GasolineEngine {
public start() { console.log("Бррм! Бензиновый двигатель запущен."); }
}
class Car {
private engine: GasolineEngine;
constructor() {
// Нарушение DI: класс САМ создает свою зависимость
this.engine = new GasolineEngine();
}
public drive() {
this.engine.start();
console.log("Машина поехала.");
}
}
const myCar = new Car();
myCar.drive();С применением Dependency Injection (Хороший код)
Мы применяем DIP (создаем интерфейс-абстракцию) и DI (передаем объект снаружи через конструктор). Теперь машина может работать с абсолютно любым двигателем.
1. Создаем абстракцию
interface Engine {
start(): void;
}2. Создаем разные виды двигателей
class GasolineEngine implements Engine {
public start() { console.log("Бррм! Бензиновый двигатель запущен."); }
}
class ElectricEngine implements Engine {
public start() { console.log("Уууууу... Электродвигатель запущен."); }
}3. Класс автомобиля использует Внедрение Зависимостей (DI)
Обратите внимание: в конструктор Car мы передаем готовый объект, который соответствует интерфейсу Engine.
class Car {
// Зависимость внедряется (injects) через конструктор
constructor(private engine: Engine) {}
public drive() {
this.engine.start(); // Работаем с абстракцией
console.log("Машина поехала.");
}
}
// === Использование (Мы сами "внедряем" зависимости вручную) ===
const petrolEngine = new GasolineEngine();
const teslaEngine = new ElectricEngine();
// Создаем бензиновую машину
const bmw = new Car(petrolEngine);
bmw.drive();
// Создаем электрокар, просто "внедряя" другой двигатель
const tesla = new Car(teslaEngine);
tesla.drive();💡 Три способа внедрения зависимостей (DI)
В примере выше мы использовали Constructor Injection (внедрение через конструктор) — это самый частый и рекомендуемый способ. Но бывают и другие:
- Constructor Injection (через конструктор): Зависимость передается при создании объекта. Объект без нее просто не создастся.
- Setter Injection (через метод/сеттер): Зависимость можно передать или заменить уже после создания объекта (например, метод
setEngine(engine: Engine)). - Interface Injection (через интерфейс): Класс реализует специальный интерфейс, который обязывает его иметь метод для приема зависимости (используется редко).
Зачем нужны DI-фреймворки (InversifyJS, NestJS)?
В больших проектах связывать объекты вручную через new Car(new ElectricEngine()) становится тяжело — кодовая база разрастается. Специальные фреймворки берут эту рутину на себя. Они автоматически смотрят на типы в конструкторе и сами создают и подставляют (внедряют) нужные объекты.
Dependency Injection (DI) в мутабельной реактивности (на примере Vue 3)
В системах с мутабельной реактивностью состояние меняется напрямую, а UI автоматически реагирует на эти изменения. Чтобы изолировать бизнес-логику от компонентов, реактивное хранилище (store) создается в виде класса или объекта, а затем внедряется в дерево компонентов через механизм Provide / Inject (аналог IoC-контейнера).
❌ Без DI: Жесткая связь (Плохой код)
Компонент напрямую импортирует конкретный экземпляр реактивного сервиса. Мы не можем подменить его на тестовый (Mock) или изменить конфигурацию для разных частей приложения.
// user-store.ts
import { reactive } from 'vue';
// Конкретный сервис, намертво завязанный на реальный fetch
class RealUserStore {
// Мутабельное реактивное состояние
public state = reactive({
name: 'Гость',
isLoading: false
});
async loadUser() {
this.state.isLoading = true;
const res = await fetch('https://example.com');
const data = await res.json();
this.state.name = data.name; // Прямая мутация реактивного свойства
this.state.isLoading = false;
}
}
// Экспортируем ОДИН конкретный созданный объект
export const realUserStore = new RealUserStore();<!-- UserProfile.vue -->
<script setup lang="ts">
import { onMounted } from 'vue';
import { realUserStore } from './user-store'; // ЖЕСТКИЙ ИМПОРТ
</script>
<template>
<div>
<p v-if="realUserStore.state.isLoading">Загрузка...</p>
<p v-else>Привет, {{ realUserStore.state.name }}</p>
<button @click="realUserStore.loadUser()">Обновить</button>
</div>
</template>
С применением DI через Provide / Inject (Хороший код)
Мы создаем абстракцию (интерфейс) для реактивного хранилища, регистрируем (provide) нужную реализацию на верхнем уровне приложения и внедряем (inject) её в компоненты.
1. Создаем интерфейс и уникальный токен (Абстракция)
// user-store.interface.ts
import { InjectionKey } from 'vue';
// Описываем, как должно выглядеть наше мутабельное реактивное состояние
export interface IUserStoreState {
name: string;
isLoading: boolean;
}
// Интерфейс самого сервиса
export interface IUserStore {
state: IUserStoreState;
loadUser(): Promise<void>;
}
// Уникальный токен для DI-контейнера Vue
export const USER_STORE_KEY: InjectionKey<IUserStore> = Symbol('USER_STORE_KEY');2. Создаем разные реализации сервиса
// real-user-store.ts
import { reactive } from 'vue';
import { IUserStore, IUserStoreState } from './user-store.interface';
export class RealUserStore implements IUserStore {
// Инициализируем мутабельную реактивность
public state = reactive<IUserStoreState>({
name: 'Гость',
isLoading: false
});
async loadUser() {
this.state.isLoading = true;
const res = await fetch('https://example.com');
const data = await res.json();
this.state.name = data.name; // Мутируем реактивные данные напрямую
this.state.isLoading = false;
}
}
// Mock-сервис для тестирования или разработки без бэкенда
export class MockUserStore implements IUserStore {
public state = reactive<IUserStoreState>({
name: 'Тестовый Админ (Offline)',
isLoading: false
});
async loadUser() {
this.state.isLoading = true;
this.state.name = 'Обновленный Тестовый Пользователь';
this.state.isLoading = false;
}
}3. Регистрируем зависимость на верхнем уровне (App.vue)
Именно здесь мы решаем, какую реализацию реактивного стора предоставить приложению.
<!-- App.vue -->
<script setup lang="ts">
import { provide } from 'vue';
import { USER_STORE_KEY } from './user-store.interface';
import { RealUserStore, MockUserStore } from './real-user-store';
import UserProfile from './UserProfile.vue';
const isDev = process.env.NODE_ENV === 'development';
// ВНЕДРЯЕМ (PROVIDE) нужный класс. Компоненты снизу получат именно его.
const userStore = isDev ? new MockUserStore() : new RealUserStore();
provide(USER_STORE_KEY, userStore);
</script>
<template>
<UserProfile />
</template>
4. Внедряем зависимость в компонент (UserProfile.vue)
Компонент теперь зависит только от интерфейса. Мутабельная реактивность продолжает работать: при вызове loadUser() состояние внутри класса изменится, и Vue мгновенно перерисует интерфейс.
<!-- UserProfile.vue -->
<script setup lang="ts">
import { inject } from 'vue';
import { USER_STORE_KEY } from './user-store.interface';
// Извлекаем (INJECT) зависимость по токену
const userStore = inject(USER_STORE_KEY);
if (!userStore) {
throw new Error('UserStore не был предоставлен сверху!');
}
</script>
<template>
<div>
<!-- Читаем данные из мутабельного реактивного стейта -->
<p v-if="userStore.state.isLoading">Загрузка...</p>
<p v-else>Привет, {{ userStore.state.name }}</p>
<button @click="userStore.loadUser()">Обновить данные</button>
</div>
</template>
Почему DI + мутабельная реактивность — это мощно?
- Изоляция логики: Компоненты превращаются в «глупые» шаблоны. Вся работа с API и мутации данных происходят в чистых TypeScript-классах.
- Идеально для тестов: При тестировании компонента
UserProfileс помощью Vue Test Utils вам достаточно передать фейковый стор в блок опций:provide: { [USER_STORE_KEY]: new MockUserStore() }. Компонент отрендерится мгновенно, не пытаясь стучаться в сеть. - Контроль жизненного цикла: Создавая экземпляры классов внутри
App.vue(или других родительских компонентов), вы можете контролировать время жизни стора — он может быть глобальным (Singleton) или создаваться заново для каждой отдельной вкладки или модального окна.
Dependency Injection (DI) и мутабельная реактивность в React (на примере Valtio)
Valtio превращает обычный объект в реактивное прокси. Вы можете напрямую мутировать (изменять) свойства этого объекта в методах класса, а специальный хук useSnapshot отследит эти мутации и автоматически перерисует компонент React.
Комбинируя классы Valtio с React Context, мы получаем чистую архитектуру с инверсией зависимостей.
❌ Без DI: Жесткая связь (Плохой код)
Компонент намертво привязан к конкретному глобальному экземпляру counterStore. Его нельзя переиспользовать с другими данными или изолированно протестировать.
import { proxy, useSnapshot } from 'valtio';
// Конкретная реализация хранилища
class CounterStore {
// Мутабельное реактивное состояние внутри Proxy
public state = proxy({ count: 0 });
public increment() {
this.state.count++; // Прямая мутация! Никаких setState()
}
}
// Экспорт глобального синглтона (жесткая привязка)
export const globalCounterStore = new CounterStore();
// Компонент
export const CounterComponent = () => {
// Хук создает реактивный слепок для рендеринга
const snap = useSnapshot(globalCounterStore.state);
return (
<div>
<p>Счетчик: {snap.count}</p>
<button @click={() => globalCounterStore.increment()}>+</button>
</div>
);
};С применением DI и мутабельной реактивности (Хороший код)
Мы создаем абстракцию (интерфейс) для стора, передаем конкретную реализацию через React Context и внедряем её в компоненты.
1. Создаем интерфейс и контекст (Абстракция)
// counter.interface.ts
import { createContext, useContext } from 'react';
export interface ICounterState {
count: number;
}
export interface ICounterStore {
state: ICounterState;
increment(): void;
}
// Создаем контекст для внедрения интерфейса
export const CounterStoreContext = createContext<ICounterStore | null>(null);
// Кастомный хук для инжекции зависимости
export const useCounterStore = () => {
const store = useContext(CounterStoreContext);
if (!store) throw new Error('useCounterStore должен быть внутри провайдера');
return store;
};2. Создаем разные детали реализации
// counter.implementations.ts
import { proxy } from 'valtio';
import { ICounterStore, ICounterState } from './counter.interface';
// Реализация №1: Стандартный счетчик, шаг = 1
export class StandardCounterStore implements ICounterStore {
public state = proxy<ICounterState>({ count: 0 });
public increment() {
this.state.count += 1; // Мутируем состояние напрямую
}
}
// Реализация №2: Супер-счетчик для вип-пользователей, шаг = 10
export class DoubleCounterStore implements ICounterStore {
public state = proxy<ICounterState>({ count: 0 });
public increment() {
this.state.count += 10; // Другая логика мутации
}
}3. Компонент (Зависит только от абстракции)
Компонент внедряет стор через хук useCounterStore. Благодаря useSnapshot, React будет автоматически обновлять UI при любой мутации внутри класса, но сам компонент ничего не знает о том, какой именно класс ему передали.
// CounterComponent.tsx
import React from 'react';
import { useSnapshot } from 'valtio';
import { useCounterStore } from './counter.interface';
export const CounterComponent = () => {
const store = useCounterStore(); // ВНЕДРЕНИЕ ЗАВИСИМОСТИ
const snap = useSnapshot(store.state); // Подписка на мутабельные изменения
return (
<div style={{ border: '1px solid black', padding: '10px', margin: '5px' }}>
<p>Значение: {snap.count}</p>
<button onClick={() => store.increment()}>Увеличить</button>
</div>
);
};4. Настройка зависимостей на верхнем уровне (App.tsx)
Теперь мы можем гибко управлять тем, какие зависимости отправляются в компоненты. Мы можем отрендерить два одинаковых компонента UI, но «внедрить» в них абсолютно разную мутабельную логику.
// App.tsx
import React from 'react';
import { CounterStoreContext } from './counter.interface';
import { StandardCounterStore, DoubleCounterStore } from './counter.implementations';
import { CounterComponent } from './CounterComponent';
// Создаем экземпляры разных сервисов
const standardStore = new StandardCounterStore();
const vipStore = new DoubleCounterStore();
export const App = () => {
return (
<div>
<h3>Обычная зона (Шаг +1):</h3>
{/* Внедряем стандартный мутабельный стор */}
<CounterStoreContext.Provider value={standardStore}>
<CounterComponent />
</CounterStoreContext.Provider>
<h3>VIP зона (Шаг +10):</h3>
{/* Внедряем другой мутабельный стор в тот же самый компонент! */}
<CounterStoreContext.Provider value={vipStore}>
<CounterComponent />
</CounterStoreContext.Provider>
</div>
);
};Главные преимущества такого подхода в React:
- Производительность:
useSnapshotот Valtio делает точечные обновления. Компонент перерисуется только тогда, когда изменится конкретное мутированное свойство, избегая лишних ререндеров React. - Переиспользование UI: Компонент
CounterComponentстал максимально абстрактным. Его можно вставить в любую часть приложения, просто обеспечив нужный контекст. - Чистые тесты: В unit-тестах компонента вам не нужно создавать сложные моки для хуков React. Вы просто передаете в
Providerобъект-заглушку с мутабельным Proxy, имитируя любые сценарии.
Dependency Injection (DI) и мутабельная реактивность в React (на примере MobX)
Комбинация MobX + React Context считается эталонным подходом для крупных enterprise-приложений. Мы заворачиваем реактивную логику в чистые TypeScript-классы, а React Context используем как легковесный IoC-контейнер для их внедрения.
❌ Нарушение DI: Глобальный синглтон (Плохой код)
Компонент намертво импортирует конкретный экземпляр todoStore. Мы не сможем создать две независимые ленты задач на одной странице или подменить стор в тестах.
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';
class TodoStore {
public todos: string[] = [];
constructor() {
makeAutoObservable(this); // Делает свойства реактивными, а методы — экшенами
}
public addTodo(task: string) {
this.todos.push(task); // Мутируем массив напрямую!
}
}
// ЖЕСТКАЯ ПРИВЯЗКА: экспорт единственного экземпляра
export const globalTodoStore = new TodoStore();
// Компонент жестко зависит от глобального объекта
export const TodoList = observer(() => {
return (
<div>
<button onClick={() => globalTodoStore.addTodo('Новая задача')}>Добавить</button>
<ul>
{globalTodoStore.todos.map((todo, i) => <li key={i}>{todo}</li>)}
</ul>
</div>
);
});С применением DI через React Context (Хороший код)
Мы создаем абстракцию (интерфейс), настраиваем Context для внедрения и пишем компонент, который вообще не зависит от конкретной реализации MobX-стора.
1. Создаем интерфейс и контекст (Абстракция)
// todo.interface.ts
import { createContext, useContext } from 'react';
export interface ITodoStore {
todos: string[];
addTodo(task: string): void;
}
// Создаем контекст, который ожидает объект, соответствующий интерфейсу
export const TodoStoreContext = createContext<ITodoStore | null>(null);
// Кастомный хук для внедрения (инжекции) зависимости в компоненты
export const useTodoStore = (): ITodoStore => {
const store = useContext(TodoStoreContext);
if (!store) throw new Error('useTodoStore должен использоваться внутри TodoStoreContext.Provider');
return store;
};2. Создаем разные детали реализации (MobX-классы)
// todo.stores.ts
import { makeAutoObservable } from 'mobx';
import { ITodoStore } from './todo.interface';
// Реализация №1: Обычный список задач
export class StandardTodoStore implements ITodoStore {
public todos: string[] = [];
constructor() {
makeAutoObservable(this); // Включает мутабельную реактивность MobX
}
public addTodo(task: string) {
this.todos.push(task); // Прямая мутация
}
}
// Реализация №2: Стор, который автоматически добавляет префикс [ВАЖНО] к задачам
export class UrgentTodoStore implements ITodoStore {
public todos: string[] = [];
constructor() {
makeAutoObservable(this);
}
public addTodo(task: string) {
this.todos.push(`🔥 [ВАЖНО] ${task}`); // Другая бизнес-логика мутации
}
}3. Создаем компонент UI (Зависит только от абстракции)
Мы оборачиваем компонент в observer из mobx-react-lite. Функция observer заставляет React автоматически перерисовывать компонент, когда мутируют наблюдаемые (observable) данные, к которым компонент обратился во время рендера.
// TodoList.tsx
import React from 'react';
import { observer } from 'mobx-react-lite';
import { useTodoStore } from './todo.interface';
// Обязательно оборачиваем в observer, чтобы React реагировал на мутации MobX
export const TodoList = observer(() => {
const store = useTodoStore(); // ВНЕДРЕНИЕ ЗАВИСИМОСТИ!
return (
<div style={{ padding: '15px', border: '1px dashed gray', marginBottom: '10px' }}>
<button onClick={() => store.addTodo('Выучить SOLID')}>
Добавить задачу
</button>
<ul>
{store.todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</div>
);
});4. Внедрение зависимостей на верхнем уровне (App.tsx)
Теперь мы можем использовать один и тот же компонент отображения TodoList, но изолированно внедрять в него совершенно разную реактивную логику и разные экземпляры данных.
// App.tsx
import React from 'react';
import { TodoStoreContext } from './todo.interface';
import { StandardTodoStore, UrgentTodoStore } from './todo.stores';
import { TodoList } from './TodoList';
// Создаем независимые экземпляры мутабельных хранилищ
const standardTodoStore = new StandardTodoStore();
const urgentTodoStore = new UrgentTodoStore();
export const App = () => {
return (
<div>
<h2>Обычный список задач:</h2>
{/* Внедряем стандартный стор */}
<TodoStoreContext.Provider value={standardTodoStore}>
<TodoList />
</TodoStoreContext.Provider>
<h2>Срочный список задач:</h2>
{/* Внедряем важный стор в тот же самый компонент */}
<TodoStoreContext.Provider value={urgentTodoStore}>
<TodoList />
</TodoStoreContext.Provider>
</div>
);
};В чем главные плюсы связки MobX + DI?
- Множественные экземпляры (Multi-instance): Благодаря отказу от глобальных синглтонов, вы можете отрендерить сколько угодно компонентов
TodoListна одной странице (например, в разных вкладках). У каждого будет свой собственный изолированный MobX-класс, внедренный через свойProvider. - Нулевая связанность с UI: Ваша бизнес-логика написана на чистом TypeScript. Вы можете полностью протестировать классы
StandardTodoStoreилиUrgentTodoStoreобычным инструментом вроде Jest или Vitest вообще без запуска React и рендеринга компонентов. - Высокая производительность: MobX автоматически выстраивает граф зависимостей. Если в
UrgentTodoStoreизменятся данные, перерисуется только тотTodoList, в который этот стор был внедрен, не затрагивая остальные компоненты приложения.