← Назад к вопросам

Для чего нужен DI?

1.8 Middle🔥 161 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Dependency Injection (DI) - Инъекция зависимостей

Что такое DI

Dependency Injection (DI) - это паттерн проектирования, при котором компонент получает свои зависимости извне (через props, параметры конструктора, хуки), а не создает их сам.

Вместо того чтобы:

// Компонент создает свои зависимости сам
function UserComponent() {
  const api = new ApiService();  // Зависимость создана внутри
  const storage = new LocalStorage();  // Зависимость создана внутри
  // ...
}

Мы передаем зависимости:

// Зависимости приходят извне
function UserComponent({ api, storage }) {
  // Используем переданные зависимости
  // ...
}

Зачем это нужно

1. Тестируемость - основная причина

Без DI - тестирование сложное:

// Сложно тестировать, потому что UserComponent зависит от реального ApiService
function UserComponent() {
  const api = new ApiService(); // Обращается к реальному серверу!
  
  useEffect(() => {
    api.getUser(123).then(setUser); // В тесте это будет реальный запрос
  }, []);
  
  return <div>{user?.name}</div>;
}

// Тест:
test('UserComponent displays user', async () => {
  render(<UserComponent />);
  // Отправится РЕАЛЬНЫЙ HTTP запрос на сервер! Медленно, нестабильно, дорого.
  await waitFor(() => expect(screen.getByText('John')).toBeInTheDocument());
});

С DI - тестирование простое:

// UserComponent не знает о деталях ApiService
function UserComponent({ api }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    api.getUser(123).then(setUser);
  }, [api]);
  
  return <div>{user?.name}</div>;
}

// Тест с мок-объектом:
test('UserComponent displays user', async () => {
  const mockApi = {
    getUser: jest.fn().mockResolvedValue({ id: 123, name: 'John' })
  };
  
  render(<UserComponent api={mockApi} />);
  await waitFor(() => expect(screen.getByText('John')).toBeInTheDocument());
  expect(mockApi.getUser).toHaveBeenCalledWith(123);
});

Делает тесты:

  • Быстрыми (не нужны настоящие запросы)
  • Надежными (не зависят от сервера)
  • Дешевыми (не требуют доступа к БД)

2. Гибкость и масштабируемость

// Интерфейс, определяющий контракт
interface ILogger {
  log(message: string): void;
  error(message: string): void;
}

// Разные реализации
class ConsoleLogger implements ILogger {
  log(message: string) { console.log(message); }
  error(message: string) { console.error(message); }
}

class FileLogger implements ILogger {
  log(message: string) { fs.writeFile('app.log', message); }
  error(message: string) { fs.writeFile('errors.log', message); }
}

// Компонент работает с любой реализацией
function UserService({ logger }: { logger: ILogger }) {
  function getUser(id: string) {
    logger.log(`Fetching user ${id}`);
    // ...
  }
  return { getUser };
}

// В продакшене используем FileLogger
const service = new UserService({ logger: new FileLogger() });

// В тестах используем мок
const mockLogger = { log: jest.fn(), error: jest.fn() };
const testService = new UserService({ logger: mockLogger });

3. Разделение ответственности

// БЕЗ DI - компонент отвечает за всё
function UserProfile() {
  // Создает API сервис
  const api = new ApiService(API_URL, API_KEY);
  
  // Создает хранилище
  const storage = new LocalStorage();
  
  // Создает logger
  const logger = new ConsoleLogger();
  
  // Логика компонента
  // ...
}

// С DI - компонент получает инструменты и фокусируется на своей задаче
function UserProfile({ api, storage, logger }) {
  // Логика компонента
  // ...
}

4. Упрощение рефакторинга

// Если менять реализацию ApiService, это не повлияет на компоненты
// Они просто получат новый объект api через props

class ApiService {
  async getUser(id: string) {
    return fetch(`/api/users/${id}`).then(r => r.json());
  }
}

// Потом меняем на что-то новое
class ApiServiceV2 {
  async getUser(id: string) {
    return graphql(`query { user(id: "${id}") { ... } }`);
  }
}

// Компоненты не меняются - им всё равно, откуда приходит api
const app = <UserProfile api={new ApiServiceV2()} />;

DI в React (практические примеры)

1. Через props

interface UserServiceProps {
  api: IApiService;
  cache: ICache;
}

function UserPage({ api, cache }: UserServiceProps) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    const cachedUser = cache.get('user_123');
    if (cachedUser) {
      setUser(cachedUser);
    } else {
      api.getUser('123').then(data => {
        cache.set('user_123', data);
        setUser(data);
      });
    }
  }, [api, cache]);
  
  return <div>{user?.name}</div>;
}

// Использование:
const app = (
  <UserPage 
    api={new ApiService()} 
    cache={new MemoryCache()} 
  />
);

2. Через Context (для глубокого дерева компонентов)

const ServiceContext = createContext<{
  api: IApiService;
  logger: ILogger;
} | null>(null);

function ServiceProvider({ children, api, logger }) {
  return (
    <ServiceContext.Provider value={{ api, logger }}>
      {children}
    </ServiceContext.Provider>
  );
}

function useServices() {
  const context = useContext(ServiceContext);
  if (!context) throw new Error('useServices must be used within ServiceProvider');
  return context;
}

// Глубокий компонент
function UserDetails() {
  const { api, logger } = useServices();
  
  useEffect(() => {
    logger.log('Loading user details');
    api.getUserDetails().then(setDetails);
  }, [api, logger]);
  
  return <div>...</div>;
}

// Использование:
const app = (
  <ServiceProvider api={new ApiService()} logger={new ConsoleLogger()}>
    <AppLayout>
      <UserDetails /> {/* Автоматически получит сервисы */}
    </AppLayout>
  </ServiceProvider>
);

3. Через кастомные хуки

// Хук который получает зависимость и управляет состоянием
function useUser(api: IApiService, userId: string) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    setLoading(true);
    api.getUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [api, userId]);
  
  return { user, loading, error };
}

// Использование с инъекцией зависимости
function UserPage({ api }: { api: IApiService }) {
  const { user, loading, error } = useUser(api, '123');
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user?.name}</div>;
}

Сравнение подходов

Без DI (тесный coupling):

function UserComponent() {
  const api = new ApiService(); // Жесткая привязка
  // Сложно тестировать
  // Сложно менять реализацию
  // Компонент отвечает за создание зависимостей
}

С DI (слабый coupling):

function UserComponent({ api }: { api: IApiService }) {
  // Легко тестировать (передаем мок)
  // Легко менять реализацию (передаем новый объект)
  // Компонент фокусируется на своей задаче
}

Когда нужен DI

Обязательно:

  • Компоненты, которые делают HTTP запросы
  • Компоненты, которые работают с хранилищем (localStorage, DB)
  • Компоненты, которые логируют события
  • Сервисы и хуки, которые зависят от внешних API

Не обязательно:

  • Чисто UI компоненты (Button, Card, Input)
  • Компоненты без побочных эффектов
  • Простые утилиты и формулы

Заключение

Dependency Injection нужна для:

  1. Тестируемости - основная причина. С DI тесты быстрые и надежные
  2. Гибкости - легко менять реализацию без изменения кода компонента
  3. Масштабируемости - проще строить большие системы с чистой архитектурой
  4. Понятности - видно, что нужно компоненту (прямо в сигнатуре функции)

В React это особенно важно, потому что упрощает тестирование благодаря мокированию зависимостей.