Dependency Injection
Dependency Injection (DI) — паттерн проектирования, при котором зависимости объекта передаются ему извне, а не создаются внутри, что делает компоненты независимыми и легко тестируемыми.
Зачем нужно
- Компоненты не привязаны к конкретным реализациям — их легко подменить (в тестах, при смене библиотеки)
- Упрощает unit-тестирование: зависимости подставляются как mock-объекты
- Снижает связность кода и следует принципу инверсии зависимостей (Dependency Inversion из SOLID)
Где используется
- Angular — встроенный DI-контейнер на основе провайдеров и декораторов
- NestJS — серверный фреймворк с DI в стиле Angular
- React — ручной DI через props (prop drilling) или Context API
- Тестирование — подмена HTTP-клиента, базы данных, сервисов на fake/mock
- Конфигурирование приложения — разные реализации для dev/prod окружений
Основной контент
Проблема без DI
class UserService {
private http = new HttpClient(); // жёсткая зависимость
getUser(id: number) {
return this.http.get(`/api/users/${id}`);
}
}
// Тестировать UserService невозможно без реального HTTP-запроса
Внедрение через конструктор (Constructor Injection)
interface HttpClient {
get(url: string): Promise<unknown>;
}
class UserService {
constructor(private http: HttpClient) {}
async getUser(id: number) {
return this.http.get(`/api/users/${id}`);
}
}
// Продакшн
const realHttp: HttpClient = new FetchHttpClient();
const userService = new UserService(realHttp);
// Тест
const fakeHttp: HttpClient = { get: async => ({ id: 1, name: "Alice" }) };
const testService = new UserService(fakeHttp);
Внедрение через фабрику (Factory Pattern + DI)
type Logger = (msg: string) => void;
function createOrderService(logger: Logger) {
return {
placeOrder(item: string) {
logger(`Order placed: ${item}`);
},
};
}
const prodService = createOrderService(console.log);
const testService = createOrderService(() => {}); // silent logger в тестах
DI через React Context
interface AuthService {
getToken: string | null;
logout: void;
}
const AuthContext = React.createContext<AuthService | null>(null);
function useAuth: AuthService {
const ctx = React.useContext(AuthContext);
if (!ctx) throw new Error("AuthContext not provided");
return ctx;
}
// Провайдер на уровне приложения
function App() {
const authService: AuthService = new RealAuthService();
return (
<AuthContext.Provider value={authService}>
<Router />
</AuthContext.Provider>
);
}
// В компоненте — получаем зависимость, не создаём
function Header() {
const auth = useAuth;
return <button onClick={auth.logout}>Выйти</button>;
}
DI-контейнер вручную (упрощённо)
class Container {
private registry = new Map<string, unknown>;
register<T>(token: string, instance: T): void {
this.registry.set(token, instance);
}
resolve<T>(token: string): T {
const instance = this.registry.get(token);
if (!instance) throw new Error(`No provider for ${token}`);
return instance as T;
}
}
const container = new Container();
container.register("logger", console.log);
container.register("userService", new UserService(new FetchHttpClient));
const service = container.resolve<UserService>("userService");
Диаграмма зависимостей
Без DI: С DI:
UserService UserService
└─ создаёт └─ получает (через конструктор/контекст)
HttpClient HttpClient (интерфейс)
├─ FetchHttpClient (prod)
└─ MockHttpClient (test)
Частые ошибки
- Создавать зависимости внутри класса —
new Serviceв конструкторе делает класс нетестируемым - Передавать весь контейнер как зависимость (Service Locator anti-pattern) — компонент знает о контейнере, связность не уменьшается
- Использовать глобальные синглтоны вместо DI — скрытые зависимости, сложно тестировать
- Не типизировать интерфейс зависимости — теряется contract между компонентами
- Слишком глубокий prop drilling в React — при 3+ уровнях лучше использовать Context или специализированный DI-контейнер
🎓 Источники
- 🎓 [Inversion of Control and Dependency Injection in Node.js] · 2018-10-09 · YouTube
- Тезисы: DI ⊂ IoC — внедрение зависимостей это частный случай инверсии управления. В Node.js DI часто реализуют через песочницы и обёртки над
require. Framework сам раскидывает зависимости в контексты компонентов. Декларативное описание зависимостей в JSON. - Альтернативная позиция: «всё писать исключительно через внедрение зависимости не стоит, потому что это неявный способ. А всегда явный способ лучше, чем неявный». DI — не серебряная пуля.
- Тезисы: DI ⊂ IoC — внедрение зависимостей это частный случай инверсии управления. В Node.js DI часто реализуют через песочницы и обёртки над
- 🎓 [9. Летняя школа 2017 — Песочницы, IoC, DI] · 2019-11-30 · YouTube
- Тезисы: Фреймворк подменяет
requireчерез VM-песочницы.safeRequire+runSandboxed— main точка входа из изолированного контекста. API подменяется вcontext.globalпесочницы.
- Тезисы: Фреймворк подменяет
- 🎓 [7. Летняя школа 2017 — DI, Firebase, Angular, SPA, React] · 2019-11-20 · YouTube
- Тезисы: DI решает проблему ориентации в большом приложении. DI в Node по сути работает как в Angular 1 — некоторые классы можно использовать одновременно на бэке и фронте.
Связанные темы
- _MOC TypeScript
- _MOC SPA
- Singleton и Factory паттерны
- Архитектура фронтенд-приложения
- Inversion of Control