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

Зачем инвертировать зависимости?

1.3 Junior🔥 141 комментариев
#JavaScript Core

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

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

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

Зачем инвертировать зависимости?

Инверсия зависимостей (Dependency Inversion) — это принцип SOLID, который позволяет снизить связанность кода и сделать его более гибким и тестируемым. Вместо того, чтобы высокоуровневый код зависел от низкоуровневого, оба зависят от абстракции.

Проблема: прямые зависимости (плохо)

// API сервис (низкоуровневый)
class HttpClient {
  async fetch(url: string) {
    const response = await fetch(url);
    return response.json();
  }
}

// Бизнес-логика (высокоуровневый)
class UserService {
  private httpClient = new HttpClient(); // ПРЯМАЯ зависимость!

  async getUser(id: string) {
    return this.httpClient.fetch(`/api/users/${id}`);
  }
}

// Использование
const userService = new UserService();
const user = await userService.getUser('123');

Проблемы:

  • UserService жёстко привязан к HttpClient
  • Нельзя подменить HttpClient на другой (например, для тестов)
  • Сложно тестировать UserService (нужен реальный HTTP)
  • Если изменить HttpClient, нужно менять UserService

Решение: инверсия зависимостей (хорошо)

Шаг 1: Создать абстракцию (интерфейс)

// Абстракция (контракт)
interface IHttpClient {
  fetch(url: string): Promise<any>;
}

// Конкретная реализация
class HttpClient implements IHttpClient {
  async fetch(url: string) {
    const response = await fetch(url);
    return response.json();
  }
}

// Бизнес-логика зависит от абстракции, не от реализации
class UserService {
  constructor(private httpClient: IHttpClient) {} // Инъекция!

  async getUser(id: string) {
    return this.httpClient.fetch(`/api/users/${id}`);
  }
}

// Использование
const httpClient = new HttpClient();
const userService = new UserService(httpClient);

Преимущества:

  • UserService не знает о HttpClient, только об интерфейсе
  • Легко подменять реализацию (для тестов, разных HTTP клиентов)
  • Слабая связанность между модулями
  • Легче тестировать

Практический пример: тестирование

С прямыми зависимостями (сложно тестировать):

class UserService {
  private httpClient = new HttpClient();

  async getUser(id: string) {
    return this.httpClient.fetch(`/api/users/${id}`);
  }
}

// Тест ДОЛЖЕН делать реальные HTTP запросы!
test('getUser', async () => {
  const service = new UserService();
  const user = await service.getUser('123'); // Медленно, зависит от сервера
  expect(user.name).toBe('Иван');
});

С инверсией зависимостей (просто тестировать):

// Mock реализация
class MockHttpClient implements IHttpClient {
  async fetch(url: string) {
    return { id: '123', name: 'Иван', email: 'ivan@example.com' };
  }
}

// Тест БЕЗ реальных HTTP запросов!
test('getUser', async () => {
  const mockHttpClient = new MockHttpClient();
  const service = new UserService(mockHttpClient); // Инъекция mock
  const user = await service.getUser('123'); // Быстро, нет сети
  expect(user.name).toBe('Иван');
});

В React: пример с dependency injection

Плохо (жёсткие зависимости):

// API service (низкоуровневый)
class ApiService {
  async getUsers() {
    const response = await fetch('/api/users');
    return response.json();
  }
}

// Компонент (высокоуровневый)
function UserList() {
  const [users, setUsers] = useState([]);
  const apiService = new ApiService(); // ПРЯМАЯ зависимость!

  useEffect(() => {
    apiService.getUsers().then(setUsers);
  }, []);

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Хорошо (инверсия зависимостей):

// Абстракция
interface IApiService {
  getUsers(): Promise<User[]>;
}

// Реальная реализация
class ApiService implements IApiService {
  async getUsers() {
    const response = await fetch('/api/users');
    return response.json();
  }
}

// Компонент зависит от абстракции
interface UserListProps {
  apiService: IApiService; // Инъекция!
}

function UserList({ apiService }: UserListProps) {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    apiService.getUsers().then(setUsers);
  }, [apiService]);

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Использование в приложении
const apiService = new ApiService();
<UserList apiService={apiService} />;

// Тестирование с mock
class MockApiService implements IApiService {
  async getUsers() {
    return [{ id: '1', name: 'Иван' }];
  }
}

test('UserList', () => {
  const mockApiService = new MockApiService();
  render(<UserList apiService={mockApiService} />);
  expect(screen.getByText('Иван')).toBeInTheDocument();
});

Context API в React (встроенная инверсия зависимостей)

// Абстракция через Context
interface IUserService {
  getUsers(): Promise<User[]>;
  createUser(user: User): Promise<User>;
}

const UserServiceContext = React.createContext<IUserService | null>(null);

// Provider компонент
function UserServiceProvider({ children }: { children: React.ReactNode }) {
  const userService: IUserService = new UserService(); // Реальная реализация

  return (
    <UserServiceContext.Provider value={userService}>
      {children}
    </UserServiceContext.Provider>
  );
}

// Hook для использования
function useUserService() {
  const context = React.useContext(UserServiceContext);
  if (!context) {
    throw new Error('useUserService должен быть в UserServiceProvider');
  }
  return context;
}

// Компонент получает зависимость из Context
function UserList() {
  const userService = useUserService(); // Инверсия!
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    userService.getUsers().then(setUsers);
  }, [userService]);

  return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

// Приложение с реальным сервисом
function App() {
  return (
    <UserServiceProvider>
      <UserList />
    </UserServiceProvider>
  );
}

// Тестирование с mock
class MockUserService implements IUserService {
  async getUsers() {
    return [{ id: '1', name: 'Иван' }];
  }
  async createUser(user: User) {
    return user;
  }
}

test('UserList', () => {
  const mockUserService = new MockUserService();
  render(
    <UserServiceContext.Provider value={mockUserService}>
      <UserList />
    </UserServiceContext.Provider>
  );
});

Factory pattern (ещё один способ)

// Абстракция
interface IUserRepository {
  getUsers(): Promise<User[]>;
}

// Конкретная реализация
class UserRepository implements IUserRepository {
  async getUsers() {
    const response = await fetch('/api/users');
    return response.json();
  }
}

// Mock реализация
class MockUserRepository implements IUserRepository {
  async getUsers() {
    return [{ id: '1', name: 'Иван' }];
  }
}

// Factory функция
type Environment = 'development' | 'testing' | 'production';

function createUserRepository(env: Environment): IUserRepository {
  if (env === 'testing') {
    return new MockUserRepository();
  }
  return new UserRepository();
}

// Использование
const env = process.env.NODE_ENV as Environment;
const userRepository = createUserRepository(env);

Ключевые выводы

Почему инвертировать зависимости:

  • Тестируемость: легко подменять реальный код на mock
  • Гибкость: менять реализацию без изменения вышестоящего кода
  • Слабая связанность: модули независимы друг от друга
  • Переиспользуемость: один сервис может иметь разные реализации
  • Масштабируемость: легче добавлять новые функции

Принцип: Зависимостей должны идти от вышестоящих модулей к абстракциям, а не от низкоуровневых модулей к вышестоящим.

Инструменты в TypeScript/React:

  • Интерфейсы (interface) как контракты
  • Dependency Injection (через конструктор или props)
  • Context API (встроенная инверсия)
  • Factory функции (создают правильные реализации)

Инверсия зависимостей — один из самых важных принципов в чистой архитектуре.

Зачем инвертировать зависимости? | PrepBro