В чем разница между Dependency Injection и Service Locator в контексте Flutter?

«В чем разница между Dependency Injection и Service Locator в контексте Flutter?» — вопрос из категории DI, который задают на 29% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В Flutter-разработке оба подхода решают задачу управления зависимостями, но с разной философией и последствиями для тестируемости и поддерживаемости кода.

Dependency Injection (Внедрение зависимостей):

Зависимости явно передаются в класс извне, обычно через конструктор:

// Слой данных
class AuthRepository {
  Future<User> login(String email, String password) async {
    // Реализация логина
  }
}

// Слой бизнес-логики с DI
class LoginService {
  final AuthRepository _authRepo;
  final AnalyticsService _analytics;

  // Зависимости явно указаны в конструкторе
  LoginService(this._authRepo, this._analytics);

  Future<void> performLogin(String email, String password) async {
    final user = await _authRepo.login(email, password);
    await _analytics.trackLogin(user.id);
  }
}

// Использование с пакетом get_it (как DI-контейнер)
final getIt = GetIt.instance;

getIt.registerFactory<AuthRepository>(() => AuthRepository());
getIt.registerFactory<AnalyticsService>(() => FirebaseAnalyticsService());
getIt.registerFactoryParam<LoginService, String, String>(
  (email, password) => LoginService(
    getIt<AuthRepository>(),
    getIt<AnalyticsService>(),
  ),
);

Service Locator (Локатор служб):

Класс сам запрашивает зависимости из глобального реестра:

// Тот же LoginService, но с Service Locator
class LoginService {
  // Зависимости скрыты - их получение внутри методов
  Future<void> performLogin(String email, String password) async {
    final authRepo = GetIt.instance<AuthRepository>(); // Явный запрос
    final analytics = GetIt.instance<AnalyticsService>();

    final user = await authRepo.login(email, password);
    await analytics.trackLogin(user.id);
  }
}

Ключевые различия:

Аспект Dependency Injection Service Locator
Явность зависимостей Видны в сигнатуре класса Скрыты внутри реализации
Тестируемость Легко мокировать через конструктор Требует настройки глобального локатора
Связность кода Низкая — зависимости извне Высокая — привязка к глобальному локатору
Читаемость Понятно, что нужно классу Неясно без изучения кода методов

Мой подход: В production-проектах я предпочитаю чистый DI через конструктор с использованием get_it как DI-контейнера, но не как Service Locator. Для этого использую иерархическую регистрацию:

// Регистрация зависимостей
void setupDependencies() {
  // Репозитории
  getIt.registerLazySingleton<AuthRepository>(() => AuthRepositoryImpl());

  // Сервисы
  getIt.registerLazySingleton<LoginService>(
    () => LoginService(getIt<AuthRepository>()), // Явная передача
  );

  // ViewModel с автоматической инъекцией
  getIt.registerFactory<LoginViewModel>(
    () => LoginViewModel(getIt<LoginService>()),
  );
}

// В виджете
class LoginPage extends StatelessWidget {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context) {
    // Получаем ViewModel с уже внедренными зависимостями
    final viewModel = getIt<LoginViewModel>();
    return LoginView(viewModel: viewModel);
  }
}

Это обеспечивает лучшую тестируемость — в тестах я могу передать моки напрямую в конструктор, не настраивая глобальное состояние.