В предыдущем посте я рассказывала о принципе разделения интерфейса (I). Сегодня разбираем последнюю букву — D (Dependency Inversion Principle), принцип инверсии зависимостей.
Что такое Dependency Inversion Principle?
Принцип инверсии зависимостей говорит о двух вещах:
✔️высокоуровневые модули не должны зависеть от низкоуровневых — и те, и другие должны зависеть от абстракций
✔️абстракции не должны зависеть от деталей, детали должны зависеть от абстракций.
По-простому: «верх» приложения (экран, бизнес-логика) не должен быть привязан к конкретным реализациям «низа» (HTTP‑клиент, база данных, SharedPreferences и другим), он должен зависеть только от интерфейсов.
Почему это важно?
Когда высокоуровневый код напрямую знает о конкретных классах нижнего уровня, это приводит к проблемам:
◾️Любое изменение реализации «внизу» (REST → gRPC, другая БД, кэш) требует правок в бизнес-логике
◾️Код сложно тестировать — приходится тянуть реальные репозитории/сети вместо заглушек
◾️Система становится хрупкой: одна деталь «внизу» ломает много кода «наверху»
◾️Нарушаются другие принципы SOLID — растет связанность, падает переиспользуемость.
Инверсия зависимостей как раз про то, чтобы «перевернуть» направление зависимости: не высокоуровневый модуль зависит от деталей, а детали зависят от контракта, который описывает высокоуровневый модуль.
❌Нарушение принципа
Рассмотрим типичный пример с авторизацией:
class AuthRepository {
Future<void> login(String email, String password) async {
// Здесь конкретная реализация:
// HTTP-запрос, парсинг ответа, сохранение токена и т.д.
}
}
class LoginViewModel {
Future<void> login(String email, String password) async {
final repo = AuthRepository(); // Жёсткая зависимость
await repo.login(email, password);
}
}Проблемы:
✖️LoginViewModel сам создает AuthRepository и жестко на него завязан
✖️Нельзя легко подменить репозиторий в тестах (например, на фейковый, который не ходит в сеть)
✖️Любое изменение механизма авторизации требует лезть в LoginViewModel
✖️Высокоуровневый модуль (view model) зависит от конкретной детали (репозитория), а не от абстракции
✅Правильное применение принципа
Введем абстракцию и будем передавать зависимость извне (через конструктор):
// Абстракция (контракт), от которой зависит верхний уровень
abstract class IAuthRepository {
Future<void> login(String email, String password);
}
// Низкоуровневая реализация для реального API
class NetworkAuthRepository implements IAuthRepository {
@override
Future<void> login(String email, String password) async {
// HTTP, обработка ошибок, сохранение токена и т.д.
}
}
// Другая реализация, например, фейковая для тестов
class FakeAuthRepository implements IAuthRepository {
@override
Future<void> login(String email, String password) async {
// Ничего не делает или имитирует успешный логин
}
}
// Высокоуровневый модуль зависит только от интерфейса
class LoginViewModel {
final IAuthRepository _authRepository;
LoginViewModel(this._authRepository);
Future<void> login(String email, String password) async {
await _authRepository.login(email, password);
}
}
Теперь:
✔️LoginViewModel не знает, как именно реализован репозиторий — он видит только интерфейс
✔️В проде можно передать NetworkAuthRepository, в тестах — FakeAuthRepository
✔️Изменения в реализации репозитория не требуют правок в бизнес-логике
✔️Мы перевернули зависимость: конкретные реализации зависят от абстракции IAuthRepository, а не наоборот
Если вы видите в коде new/() внутри бизнес‑логики или ViewModel, создающий конкретные репозитории, сервисы и клиенты, — это хороший сигнал задуматься: не пора ли ввести интерфейс и развернуть зависимость?
✍️Повторим все принципы SOLID:
Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
