Dependency Injection (DI, внедрение зависимостей) — техника, при которой объект получает свои зависимости извне, а не создаёт их самостоятельно. DI является реализацией принципа IoC (Inversion of Control) и лежит в основе Dagger, Hilt и Swinject. Внедрение зависимостей уменьшает связность кода, упрощает тестирование и делает архитектуру гибкой. В Android DI — стандарт через Dagger Hilt от Google, в iOS — через Swinject или ручное внедрение. Подробнее — в Android DI Guide.
Главное
Dependency Injection — техника, при которой объект получает зависимости (сервисы, репозитории, конфигурации) через конструктор, сеттер или интерфейс, а не создаёт их сам через new. Цель DI — уменьшить связанность (coupling) между классами. Если класс создаёт зависимости сам, он жёстко привязан к конкретным реализациям, что усложняет тестирование и модификацию. При DI класс работает с абстракцией (protocol/interface), а конкретная реализация подставляется извне.
Три способа внедрения — Constructor Injection (через init/constructor), Setter Injection (через свойство/сеттер), Interface Injection (через метод интерфейса). Constructor Injection — предпочтительный способ: зависимости явно видны в сигнатуре, объект всегда создаётся в валидном состоянии. Setter Injection используется для опциональных зависимостей с default-значением. Interface Injection — редко, в основном для DI-контейнеров.
| Вид DI | Способ | Когда использовать | Пример |
|---|---|---|---|
| Constructor | Параметры инициализатора | Обязательные зависимости | init(service: ServiceProtocol) |
| Property | Свойство класса | Опциональные зависимости | var service: ServiceProtocol? |
| Method | Параметр метода | Временные зависимости | func doWork(with service: Service) |
DI-контейнер — библиотека, управляющая созданием и жизненным циклом зависимостей. Контейнер содержит регистрацию типов (каждый абстрактный тип сопоставлен с конкретной реализацией) и фабрику для создания объектов с разрешёнными зависимостями. В Android — Dagger/Hilt, в iOS — Swinject, Needle, Dip. Контейнер может управлять скоупом (Scope): синглтон (одна инстанция на приложение), скоуп фичи (на экран) или новый объект при каждом запросе.
Dagger Hilt — обёртка над Dagger от Google, стандартная библиотека DI для Android. Hilt упрощает Dagger: убирает ручное создание компонентов, добавляет @HiltAndroidApp, @AndroidEntryPoint и @Module. Hilt интегрируется с жизненным циклом Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver могут получать зависимости через аннотации. Кодогенерация происходит на этапе компиляции — Dagger генерирует имплементации компонентов, что даёт нулевой runtime overhead.
// Application class
@HiltAndroidApp
class MyApp : Application()
// Module — определяет, как создавать зависимости
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()
@Provides
@Singleton
fun provideApiService(client: OkHttpClient): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.build()
.create(ApiService::class.java)
}
}
// ViewModel получает зависимость через конструктор
@HiltViewModel
class MainViewModel @Inject constructor(
private val apiService: ApiService
) : ViewModel() {
private val _state = MutableStateFlow(MainState.Loading)
val state: StateFlow<MainState> = _state.asStateFlow()
fun loadData() {
viewModelScope.launch {
_state.value = MainState.Success(apiService.getData())
}
}
}
// Activity — @AndroidEntryPoint включает DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
}
Dagger компоненты и области видимости — @Singleton (на всё приложение), @ActivityScoped (на Activity), @FragmentScoped (на Fragment), @ViewModelScoped (на ViewModel). Выбор скоупа определяет время жизни объекта. @Singleton — одна инстанция на процесс, подходит для OkHttpClient и базы данных. @ActivityScoped — объект живёт пока жива Activity, для экранных зависимостей. @ViewModelScoped — новинка Hilt 2.45+, объект живёт пока жив ViewModel, что удобно для корутинных скоупов.
Swinject — популярный DI-фреймворк для iOS с открытым исходным кодом. Swinject предоставляет Container, Assemblies и различные скоупы. В отличие от Dagger, Swinject работает в рантайме — зависимости разрешаются динамически без кодогенерации. Это делает Swinject проще в настройке, но сложности с Debug: ошибка неразрешённой зависимости проявляется только в рантайме. Swinject поддерживает Constructor Injection, Property Injection и Method Injection.
import Swinject
// Assembly — группа регистраций
class NetworkAssembly: Assembly {
func assemble(container: Container) {
container.register(NetworkServiceProtocol.self) { _ in
NetworkService()
}.inObjectScope(.container) // singleton
container.register(UserRepositoryProtocol.self) { r in
UserRepository(
networkService: r.resolve(NetworkServiceProtocol.self)!
)
}
}
}
// ViewModel через Constructor Injection
class ProfileViewModel: ObservableObject {
private let repository: UserRepositoryProtocol
init(repository: UserRepositoryProtocol) {
self.repository = repository
}
@Published var user: User?
func loadUser() {
repository.fetchUser { [weak self] user in
self?.user = user
}
}
}
// Настройка DI в AppDelegate или App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!
// Property Injection для UIKit ViewController
container.register(UserViewController.self) { r in
let vc = UserViewController()
vc.viewModel = r.resolve(ProfileViewModel.self)
return vc
}
Swinject скоупы — .transient (новый объект каждый раз), .container (синглтон на контейнер), .graph (по умолчанию — объект разделяется внутри одного графа зависимостей). Для iOS-приложений достаточно .container и .transient. Swinject также поддерживает Assembler — группировку Assembly для модульной архитектуры. Для тестов Assembly заменяются на MockAssembly, что даёт подмену зависимостей без изменения продакшен-кода.
DI vs Service Locator — оба паттерна решают задачу управления зависимостями, но по-разному. DI внедряет зависимости в объект, Service Locator предоставляет глобальный реестр, из которого объект сам запрашивает зависимости. DI явно объявляет зависимости через конструктор (или сеттер). Service Locator скрывает зависимости — они запрашиваются внутри метода, что делает сигнатуру менее информативной. DI легче тестируется: достаточно подставить mock в конструктор. Service Locator требует настройки глобального реестра для каждого теста.
| Характеристика | Dependency Injection | Service Locator | Ручное внедрение |
|---|---|---|---|
| Явность зависимостей | В конструкторе | Скрыты в теле метода | Явные |
| Тестирование | Mock в конструктор | Настройка Locator | Mock в конструктор |
| Сложность настройки | Требует DI-контейнер | Глобальный реестр | Ручное создание |
| Runtime overhead | Dagger — compile-time | Runtime lookup | Нет |
DI vs ручное внедрение — без DI-контейнера зависимости создаются вручную в фабриках или AppDelegate. Для 5-10 классов ручное внедрение проще — не требует изучения Dagger или Swinject. Для 50+ классов ручное внедрение становится проблемой: конструкторы с 5-6 параметрами, сложный порядок создания, дублирование кода. DI-контейнер автоматизирует эти процессы и даёт чёткий lifecycle. Ручное внедрение без контейнера — хороший выбор для небольших проектов и прототипов.
Constructor Injection — стандарт. Всегда используйте Constructor Injection для обязательных зависимостей. Это делает зависимости явными и объект всегда готов к работе. Setter Injection — только для опциональных зависимостей (например, delegate или listener). Interface Injection — не используйте, если только не пишете собственную DI-библиотеку. Constructor Injection — единственный способ гарантировать, что объект создаётся в валидном состоянии.
Один класс — одна ответственность. Если конструктор класса требует 5+ параметров, вероятно, класс нарушает Single Responsibility Principle. Разделите класс на несколько с меньшим количеством зависимостей. Признак: если вы пишете class ServiceManager с 6 разными сервисами — это антипаттерн God Object. Вынесите бизнес-логику в Use Cases (Interactors), каждый с 1-2 зависимостями.
// ❌ Плохо: 6 зависимостей — God Object
class ProfileViewModel @Inject constructor(
private val api: ApiService,
private val db: Database,
private val analytics: Analytics,
private val prefs: Preferences,
private val location: LocationProvider,
private val notification: NotificationManager
)
// ✅ Хорошо: Use Cases с 1-2 зависимостями
class ProfileViewModel @Inject constructor(
private val loadProfileUseCase: LoadProfileUseCase,
private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)
Scope и lifecycle — выбирайте правильный скоуп для каждой зависимости. Синглтоны: OkHttpClient, база данных, SharedPreferences. Feature-скоуп: репозитории, Use Cases (если они не имеют состояния). Transient: Value Objects, DateFormatter, парсеры. Ошибка скоупинга — частая проблема: синглтон, хранящий состояние экрана, приводит к утечкам. В Android Hilt @ActivityScoped решает эту проблему, в Swinject — .container с осторожностью.
Часто задаваемые вопросы
new создаёт жёсткую связь между классами — вы не можете подменить реализацию без изменения кода. Трудно тестировать: нельзя вставить mock вместо реального сервиса. Нарушается SRP: класс отвечает и за бизнес-логику, и за создание зависимостей. DI решает эти проблемы через внедрение зависимостей извне и работу с абстракциями.
Dagger Hilt — стандарт от Google, compile-time DI с кодогенерацией, лучшая производительность и интеграция с Jetpack. Koin — runtime DI, проще в настройке, но медленнее и с ошибками в рантайме. Выбирайте Hilt для продакшен-проектов. Koin подходит для прототипов и маленьких приложений.
Нет. Для iOS доступны: Swinject (runtime, популярный), Needle (compile-time от Uber), Dip (легковесный), Weaver (Sourcery-based). Apple не предоставляет встроенный DI-контейнер, но ручное внедрение через init — стандартная практика. Для SwiftUI часто достаточно ручного DI через Environment или @StateObject без внешних библиотек.
Да. Ручное внедрение через конструктор — это DI без фреймворка. Service Locator — альтернатива без фреймворка. Фабрики и Factory Method — тоже форма DI. Фреймворк (Dagger, Swinject) автоматизирует рутинную регистрацию и разрешение зависимостей, но для 10-20 классов ручного DI достаточно.
DI — это техника (шаблон), реализующая принцип Inversion of Control. В отличие от GoF-паттернов, DI не имеет строгой структуры из 3-4 классов. DI — способ организации зависимостей, а не паттерн проектирования. DI-контейнеры (Dagger, Swinject) — это фреймворки, автоматизирующие эту технику.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также