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 — не серебряная пуля.
  • 🎓 [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 — некоторые классы можно использовать одновременно на бэке и фронте.

Связанные темы

Ресурсы