Dependency Injection: что это такое, внедрение зависимостей в iOS и Android

Автор: IT Sectr Опубликовано: 2026-02-18 Время чтения: 9 мин

Dependency Injection (DI, внедрение зависимостей) — техника, при которой объект получает свои зависимости извне, а не создаёт их самостоятельно. DI является реализацией принципа IoC (Inversion of Control) и лежит в основе Dagger, Hilt и Swinject. Внедрение зависимостей уменьшает связность кода, упрощает тестирование и делает архитектуру гибкой. В Android DI — стандарт через Dagger Hilt от Google, в iOS — через Swinject или ручное внедрение. Подробнее — в Android DI Guide.

Главное

  • Dependency Injection — зависимости передаются объекту извне, а не создаются внутри
  • Inversion of Control — DI реализует принцип IoC, управление потоком передаётся контейнеру
  • Dagger Hilt — стандарт DI для Android, основанный на Dagger от Google
  • Swinject — популярный DI-фреймворк для iOS и Swift
  • Уменьшение связности — класс зависит от абстракций, а не от конкретных реализаций

Что такое Dependency Injection: суть и виды DI

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: DI для Android с кодогенерацией

Dagger Hilt — обёртка над Dagger от Google, стандартная библиотека DI для Android. Hilt упрощает Dagger: убирает ручное создание компонентов, добавляет @HiltAndroidApp, @AndroidEntryPoint и @Module. Hilt интегрируется с жизненным циклом Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver могут получать зависимости через аннотации. Кодогенерация происходит на этапе компиляции — Dagger генерирует имплементации компонентов, что даёт нулевой runtime overhead.

kotlin
// 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 на Swift

Swinject — популярный DI-фреймворк для iOS с открытым исходным кодом. Swinject предоставляет Container, Assemblies и различные скоупы. В отличие от Dagger, Swinject работает в рантайме — зависимости разрешаются динамически без кодогенерации. Это делает Swinject проще в настройке, но сложности с Debug: ошибка неразрешённой зависимости проявляется только в рантайме. Swinject поддерживает Constructor Injection, Property Injection и Method Injection.

swift
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 с Service Locator и ручным внедрением

DI vs Service Locator — оба паттерна решают задачу управления зависимостями, но по-разному. DI внедряет зависимости в объект, Service Locator предоставляет глобальный реестр, из которого объект сам запрашивает зависимости. DI явно объявляет зависимости через конструктор (или сеттер). Service Locator скрывает зависимости — они запрашиваются внутри метода, что делает сигнатуру менее информативной. DI легче тестируется: достаточно подставить mock в конструктор. Service Locator требует настройки глобального реестра для каждого теста.

ХарактеристикаDependency InjectionService LocatorРучное внедрение
Явность зависимостейВ конструктореСкрыты в теле методаЯвные
ТестированиеMock в конструкторНастройка LocatorMock в конструктор
Сложность настройкиТребует DI-контейнерГлобальный реестрРучное создание
Runtime overheadDagger — compile-timeRuntime lookupНет

DI vs ручное внедрение — без DI-контейнера зависимости создаются вручную в фабриках или AppDelegate. Для 5-10 классов ручное внедрение проще — не требует изучения Dagger или Swinject. Для 50+ классов ручное внедрение становится проблемой: конструкторы с 5-6 параметрами, сложный порядок создания, дублирование кода. DI-контейнер автоматизирует эти процессы и даёт чёткий lifecycle. Ручное внедрение без контейнера — хороший выбор для небольших проектов и прототипов.

Лучшие практики Dependency Injection

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 зависимостями.

kotlin
// ❌ Плохо: 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 с осторожностью.

Часто задаваемые вопросы

Зачем нужен DI, если можно просто создавать new?

new создаёт жёсткую связь между классами — вы не можете подменить реализацию без изменения кода. Трудно тестировать: нельзя вставить mock вместо реального сервиса. Нарушается SRP: класс отвечает и за бизнес-логику, и за создание зависимостей. DI решает эти проблемы через внедрение зависимостей извне и работу с абстракциями.

Dagger Hilt или Koin — что выбрать для Android?

Dagger Hilt — стандарт от Google, compile-time DI с кодогенерацией, лучшая производительность и интеграция с Jetpack. Koin — runtime DI, проще в настройке, но медленнее и с ошибками в рантайме. Выбирайте Hilt для продакшен-проектов. Koin подходит для прототипов и маленьких приложений.

Swinject — единственный DI для iOS?

Нет. Для iOS доступны: Swinject (runtime, популярный), Needle (compile-time от Uber), Dip (легковесный), Weaver (Sourcery-based). Apple не предоставляет встроенный DI-контейнер, но ручное внедрение через init — стандартная практика. Для SwiftUI часто достаточно ручного DI через Environment или @StateObject без внешних библиотек.

Можно ли использовать DI без фреймворка?

Да. Ручное внедрение через конструктор — это DI без фреймворка. Service Locator — альтернатива без фреймворка. Фабрики и Factory Method — тоже форма DI. Фреймворк (Dagger, Swinject) автоматизирует рутинную регистрацию и разрешение зависимостей, но для 10-20 классов ручного DI достаточно.

DI — это паттерн или принцип?

DI — это техника (шаблон), реализующая принцип Inversion of Control. В отличие от GoF-паттернов, DI не имеет строгой структуры из 3-4 классов. DI — способ организации зависимостей, а не паттерн проектирования. DI-контейнеры (Dagger, Swinject) — это фреймворки, автоматизирующие эту технику.

Итоги

  • DI — техника внедрения зависимостей извне через конструктор, сеттер или метод
  • Dagger Hilt — стандарт DI для Android с compile-time кодогенерацией и @HiltViewModel
  • Swinject — runtime DI для iOS с Container, Assembly и скоупами
  • Constructor Injection — предпочтительный способ для обязательных зависимостей
  • Scope — Singleton для безсостоянийных сервисов, Feature-скоуп для экранных зависимостей
  • Тестирование — DI упрощает подмену зависимостей на mock без изменения кода

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также