Dependency Injection в Swift: принципы, примеры и реализация

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое Dependency Injection и зачем он нужен в Swift

Dependency Injection (DI) — это паттерн проектирования, при котором объект получает свои зависимости извне, а не создает их самостоятельно. В контексте Swift для iOS и macOS DI помогает сделать код более гибким, тестируемым и поддерживаемым. Вместо жесткого связывания классов через init() или let вы передаете зависимости через конструктор, свойство или метод.

Основные преимущества DI:

  • Тестируемость: вы легко подменяете реальные объекты на моки или стабы в юнит-тестах.
  • Слабая связанность: классы зависят от абстракций (протоколов), а не от конкретных реализаций.
  • Упрощение рефакторинга: изменения в одной части кода не ломают другую.
  • Чистая архитектура: код следует принципу единственной ответственности и принципу инверсии зависимостей (DIP из SOLID).

В Swift DI особенно важен при работе с UIKit, SwiftUI, CoreData, сетью и другими сервисами. Без него вы рискуете получить "божественные объекты" и трудности с переиспользованием кода.

Основные способы внедрения зависимостей в Swift

1. Внедрение через инициализатор (Constructor Injection)

Самый распространенный и рекомендуемый способ. Зависимости передаются при создании объекта и становятся неизменяемыми (let).

protocol NetworkServiceProtocol {
    func fetchData() -> String
}

class NetworkService: NetworkServiceProtocol {
    func fetchData() -> String {
        return "Данные с сервера"
    }
}

class ViewModel {
    private let networkService: NetworkServiceProtocol

    // Внедрение через инициализатор
    init(networkService: NetworkServiceProtocol) {
        self.networkService = networkService
    }

    func loadData() -> String {
        return networkService.fetchData()
    }
}

// Использование
let service = NetworkService()
let viewModel = ViewModel(networkService: service)
print(viewModel.loadData()) // "Данные с сервера"

Преимущества: зависимости явные, объект всегда в валидном состоянии. Недостаток: при большом количестве зависимостей конструктор становится громоздким.

2. Внедрение через свойство (Property Injection)

Зависимость устанавливается после создания объекта. Часто используется для опциональных сервисов или при работе с Storyboard/XIB.

class UserViewController: UIViewController {
    var analyticsService: AnalyticsServiceProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()
        analyticsService?.trackEvent("Экран открыт")
    }
}

// Внедрение
let vc = UserViewController()
vc.analyticsService = AnalyticsService()

Минус: свойство должно быть опциональным или иметь значение по умолчанию, что может привести к runtime-ошибкам, если забыть установить зависимость.

3. Внедрение через метод (Method Injection)

Зависимость передается в качестве параметра метода. Подходит для ситуаций, когда зависимость нужна только для одной операции.

class DataManager {
    func processData(using service: NetworkServiceProtocol) -> String {
        let rawData = service.fetchData()
        return "Обработано: \(rawData)"
    }
}

let manager = DataManager()
let result = manager.processData(using: NetworkService())
print(result) // "Обработано: Данные с сервера"

Пример реализации DI в iOS-приложении

Рассмотрим реальный пример: приложение для отображения списка пользователей. Создадим протоколы и классы с внедрением зависимостей.

// 1. Протоколы
protocol UserRepositoryProtocol {
    func getUsers() -> [String]
}

protocol UserViewModelProtocol {
    var users: [String] { get }
    func loadUsers()
}

// 2. Реализация репозитория
class UserRepository: UserRepositoryProtocol {
    func getUsers() -> [String] {
        return ["Анна", "Борис", "Виктор"]
    }
}

// 3. ViewModel с DI через инициализатор
class UserViewModel: UserViewModelProtocol {
    private let repository: UserRepositoryProtocol
    var users: [String] = []

    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }

    func loadUsers() {
        users = repository.getUsers()
    }
}

// 4. ViewController (UIKit) с DI
class UserListViewController: UIViewController {
    private let viewModel: UserViewModelProtocol

    init(viewModel: UserViewModelProtocol) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) не реализован. Используйте init(viewModel:)")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel.loadUsers()
        print("Пользователи: \(viewModel.users)")
    }
}

// Сборка приложения (Composition Root)
let repository = UserRepository()
let viewModel = UserViewModel(repository: repository)
let viewController = UserListViewController(viewModel: viewModel)
// Далее передаем viewController в window.rootViewController

Обратите внимание: мы используем протоколы UserRepositoryProtocol и UserViewModelProtocol. Это позволяет легко подменять реализации для тестирования или замены источника данных.

DI-контейнеры и библиотеки для Swift

Для больших проектов ручное внедрение через инициализаторы становится сложным. На помощь приходят DI-контейнеры — специальные объекты, которые управляют созданием и жизненным циклом зависимостей.

Swinject

Самая популярная библиотека DI для Swift. Позволяет регистрировать зависимости и получать их через контейнер.

import Swinject

let container = Container()
container.register(NetworkServiceProtocol.self) { _ in NetworkService() }
container.register(ViewModel.self) { r in
    ViewModel(networkService: r.resolve(NetworkServiceProtocol.self)!)
}

// Использование
let viewModel = container.resolve(ViewModel.self)!
print(viewModel.loadData())

Самописный простой контейнер

Если не хотите подключать сторонние библиотеки, можно создать свой минимальный DI-контейнер:

class DIContainer {
    private var services: [String: Any] = [:]

    func register(_ type: T.Type, factory: @escaping () -> T) {
        let key = String(describing: type)
        services[key] = factory
    }

    func resolve(_ type: T.Type) -> T? {
        let key = String(describing: type)
        guard let factory = services[key] as? () -> T else { return nil }
        return factory()
    }
}

// Использование
let di = DIContainer()
di.register(NetworkServiceProtocol.self) { NetworkService() }
let service = di.resolve(NetworkServiceProtocol.self)
print(service?.fetchData() ?? "Нет сервиса")

Контейнеры особенно полезны при работе с модульными приложениями, где зависимости нужно настраивать централизован

Блоги

Рекомендаций по книгам