Dependency Injection (DI, инжектиране на зависимости) — техника, при която обектът получава своите зависимости отвън, вместо да ги създава сам. DI е имплементация на принципа IoC (Inversion of Control) и стои в основата на Dagger, Hilt и Swinject. Инжектирането на зависимости намалява свързаността на кода, опростява тестването и прави архитектурата гъвкава. В Android DI е стандарт чрез Dagger Hilt от Google, в iOS — чрез Swinject или ръчно инжектиране. Повече — в Android DI Guide.
Основни положения
Dependency Injection — техника, при която обектът получава зависимости (услуги, хранилища, конфигурации) чрез конструктор, setter или интерфейс, вместо да ги създава сам чрез new. Целта на DI е да намали свързаността (coupling) между класове. Ако класът сам създава зависимостите си, той е тясно свързан с конкретни имплементации, което затруднява тестването и модификацията. При DI класът работи с абстракция (protocol/interface), а конкретната имплементация се доставя отвън.
Три начина на инжектиране — Constructor Injection (чрез init/constructor), Setter Injection (чрез свойство/setter), 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.
// 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, удобно за coroutine обхвати.
Swinject — популярна DI рамка за iOS с отворен код. Swinject предоставя Container, Assemblies и различни обхвати. За разлика от Dagger, Swinject работи в runtime — зависимостите се разрешават динамично без генериране на код. Това прави Swinject по-лесен за конфигуриране, но затруднява дебъгването: грешка от неразрешена зависимост се проявява едва в runtime. 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 декларира изрично зависимостите чрез конструктор (или setter). 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. Разделете класа на няколко с по-малко зависимости. Признак: ако пишете клас 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
)
Обхват и lifecycle — изберете правилния обхват за всяка зависимост. Сингълтъни: OkHttpClient, база данни, SharedPreferences. Обхват на функция: хранилища, 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, по-лесен за конфигуриране, но по-бавен и с грешки в runtime. Изберете Hilt за продукционни проекти. Koin е подходящ за прототипи и малки приложения.
Не. За iOS са налични: Swinject (runtime, популярен), Needle (compile-time от Uber), Dip (лек), Weaver (базиран на Sourcery). 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също