Dependency Injection

Dependency Injection (DI) — это шаблон проектирования, который позволяет отделить создание зависимостей от их использования, облегчая тестирование и повышая модульность кода. В контексте Preact, где компоненты тесно связаны с состоянием и жизненным циклом, DI становится особенно полезным для управления сервисами, конфигурациями и внешними зависимостями.

Принцип работы Dependency Injection

DI строится на трёх ключевых принципах:

  1. Инверсия управления (Inversion of Control, IoC) Компонент не создаёт свои зависимости напрямую. Вместо этого они предоставляются извне, что уменьшает связанность и упрощает замену реализации.

  2. Внедрение зависимостей (Injection) Зависимости могут передаваться через:

    • Конструктор компонента
    • Пропсы
    • Контекст (Context API в Preact)
  3. Абстракция зависимостей Компоненты работают с интерфейсами или абстракциями, а не с конкретными реализациями, что облегчает модульное тестирование.

Использование DI через пропсы

Наиболее простой способ внедрения зависимостей в Preact — передача сервисов через пропсы.

import { h } from 'preact';

function LoggerService() {
  this.log = (msg) => console.log(`[LOG]: ${msg}`);
}

function App({ logger }) {
  logger.log('Приложение инициализировано');
  return <div>Привет, Preact!</div>;
}

// Внедрение зависимости при рендере
<App logger={new LoggerService()} />;

Преимущества такого подхода:

  • Простота реализации
  • Явная передача зависимостей

Недостатки:

  • При большом количестве компонентов и сервисов пропсы становятся громоздкими
  • Сложнее управлять глобальными зависимостями

Контекст как инструмент DI

Preact поддерживает контекст через createContext, что позволяет внедрять зависимости глубоко в иерархию компонентов без передачи через пропсы на каждом уровне.

import { h, createContext } from 'preact';
import { useContext } from 'preact/hooks';

const LoggerContext = createContext(null);

function App() {
  return (
    <LoggerContext.Provider value={new LoggerService()}>
      <ChildComponent />
    </LoggerContext.Provider>
  );
}

function ChildComponent() {
  const logger = useContext(LoggerContext);
  logger.log('ChildComponent загружен');
  return <div>Дочерний компонент</div>;
}

Плюсы использования контекста:

  • Упрощение передачи зависимостей через дерево компонентов
  • Централизованное управление сервисами

Минусы:

  • Сложнее отследить, какие зависимости реально используются в компоненте
  • Потенциальное нарушение принципа явной зависимости

Инъекция зависимостей через фабрики

Для более сложных приложений часто используют фабрики или сервис-локаторы, которые создают и управляют жизненным циклом зависимостей.

class ServiceFactory {
  constructor() {
    this.services = new Map();
  }

  register(key, instance) {
    this.services.set(key, instance);
  }

  get(key) {
    return this.services.get(key);
  }
}

const factory = new ServiceFactory();
factory.register('logger', new LoggerService());

function App() {
  const logger = factory.get('logger');
  logger.log('Приложение через фабрику');
  return <div>DI с фабрикой</div>;
}

Плюсы:

  • Централизованное управление зависимостями
  • Возможность замены реализаций на лету
  • Легче масштабировать

Минусы:

  • Сервис-локатор может привести к скрытой связанности
  • Потеря явности зависимости

Внедрение зависимостей в хуки

Компоненты, построенные с хуками, также могут использовать DI. Контекст и фабрики идеально подходят для предоставления зависимостей в функциональные компоненты:

import { useState, useEffect, useContext } from 'preact/hooks';

function useLogger() {
  const logger = useContext(LoggerContext);
  useEffect(() => {
    logger.log('Hook инициализирован');
  }, []);
  return logger;
}

function HookComponent() {
  const logger = useLogger();
  return <div>Использование Logger через hook</div>;
}

Тестируемость с Dependency Injection

DI упрощает написание юнит-тестов. Компоненты легко тестировать, подставляя моки вместо реальных сервисов.

const mockLogger = { log: jest.fn() };
<App logger={mockLogger} />;
// Проверка вызова log
expect(mockLogger.log).toHaveBeenCalledWith('Приложение инициализировано');

Использование DI снижает связанность компонентов и делает архитектуру приложения более гибкой. В Preact паттерн помогает управлять сервисами, облегчает тестирование и масштабирование проектов без потери производительности или читаемости кода.