Впровадження залежностей (DI) — техніка, за якої об'єкт отримує свої залежності ззовні, а не створює їх самостійно. DI є реалізацією принципу IoC (Inversion of Control) і лежить в основі Dagger, Hilt та Swinject. Впровадження залежностей зменшує зв'язність коду, спрощує тестування та робить архітектуру гнучкою. В Android DI — стандарт через Dagger Hilt від Google, в iOS — через Swinject або ручне впровадження. Детальніше — у Android DI Guide.
Головне
Впровадження залежностей — техніка, за якої об'єкт отримує залежності (сервіси, репозиторії, конфігурації) через конструктор, сеттер або інтерфейс, а не створює їх сам через new. Мета DI — зменшити зв'язаність (coupling) між класами. Якщо клас створює залежності сам, він жорстко прив'язаний до конкретних реалізацій, що ускладнює тестування та модифікацію. При DI клас працює з абстракцією (protocol/interface), а конкретна реалізація підставляється ззовні.
Три способи впровадження — Constructor Injection (через init/constructor), Setter Injection (через властивість/сеттер), Interface Injection (через метод інтерфейсу). Constructor Injection — найкращий спосіб: залежності явно видно в сигнатурі, об'єкт завжди створюється у валідному стані. Setter Injection використовується для опціональних залежностей зі значенням за замовчуванням. 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.
// Клас додатку
@HiltAndroidApp
class MyApp : Application()
// Модуль — визначає, як створювати залежності
@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 або додатку
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+ параметрів, ймовірно, клас порушує принцип єдиної відповідальності. Розділіть клас на декілька з меншою кількістю залежностей. Ознака: якщо ви пишете клас 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також