В чем разница между Dependency Injection и Service Locator в контексте 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);
  }
}

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

Ответ 18+ 🔞

Слушай, давай разберём эту дичь про зависимости во 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);
  }
}

// Настройка DI-контейнера (возьмём get_it)
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
Видимость зависимостей Всё на виду, как в витрине Спрятано, нихуя не понятно
Тестируемость Легко как два пальца — суёшь моки в конструктор Ёперный театр! Надо настраивать глобальный контейнер для каждого теста
Связанность кода Слабая, класс не привязан к внешнему миру Крепкая, как сука, к глобальному локатору
Понятность Сразу видно, что классу нужно Чтобы понять, надо копаться в методах — доверия ебать ноль

Мой вердикт, блядь

В нормальных, продакшен-проектах я использую чистый DI через конструктор. GetIt беру только как контейнер для регистрации, а не как 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);
  }
}

Почему так? Тестируемость, ёбана! В тестах я просто создаю сервис с моками, и мне да похуй на глобальное состояние. Не надо танцевать с бубном вокруг GetIt.instance. Код становится чище, прозрачнее, и когда через полгода надо что-то менять, ты не охуеешь от того, что зависимости раскиданы по всем методам, как тараканы по кухне.