Dependency Injection в Swift: практическое руководство для iOS

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

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

Начать курс

Введение: Почему Dependency Injection — это необходимость

В современной разработке под iOS и macOS чистота и тестируемость кода выходят на первый план. Один из ключевых принципов, позволяющих достичь этих целей — Dependency Injection (DI) или внедрение зависимостей. Без DI классы часто становятся "жестко сцепленными" (tightly coupled), что затрудняет их повторное использование, модификацию и, самое главное, написание юнит-тестов.

На практике Dependency Injection в Swift означает, что объект получает свои зависимости извне, а не создает их сам. Это может показаться незначительным изменением, но оно кардинально меняет архитектуру приложения. В этой статье мы разберем основные паттерны DI на Swift: Constructor Injection, Property Injection и использование DI-контейнера на примере Swinject. Каждый метод будет проиллюстрирован практическими примерами для iOS/macOS.

Готовы сделать ваш код более гибким и тестируемым? Тогда начнем.

1. Constructor Injection: Самый надежный способ

Constructor Injection — это внедрение зависимостей через инициализатор (init) класса. Этот метод считается самым предпочтительным, так как он гарантирует, что объект всегда находится в валидном состоянии: все его зависимости обязательны и неизменны.

Рассмотрим пример. Допустим, у нас есть класс NetworkService, который отвечает за HTTP-запросы, и класс UserViewModel, который использует этот сервис.

// Протокол для абстракции
protocol NetworkServiceProtocol {
    func fetchUsers() async throws -> [User]
}

// Реальная реализация
class RealNetworkService: NetworkServiceProtocol {
    func fetchUsers() async throws -> [User] {
        // Реальный HTTP-запрос
        return []
    }
}

// ViewModel с Constructor Injection
class UserViewModel {
    private let networkService: NetworkServiceProtocol

    // Зависимость передается через инициализатор
    init(networkService: NetworkServiceProtocol) {
        self.networkService = networkService
    }

    func loadUsers() async {
        do {
            let users = try await networkService.fetchUsers()
            print("Loaded \(users.count) users")
        } catch {
            print("Error: \(error)")
        }
    }
}

// Использование
let service = RealNetworkService()
let viewModel = UserViewModel(networkService: service)

Плюсы:

  • Зависимость явно объявлена и обязательна.
  • Объект не может быть создан без зависимостей.
  • Идеально подходит для юнит-тестов: легко подменить реальную реализацию на Mock.

Минусы:

  • Если зависимостей много, инициализатор становится громоздким.
  • Не подходит для опциональных зависимостей.

Этот метод — "золотой стандарт" DI. Используйте его везде, где это возможно.

2. Property Injection: Гибкость для опциональных зависимостей

Property Injection — это внедрение зависимостей через свойства (property) объекта. Обычно используется, когда зависимость не является обязательной для работы объекта или когда ее невозможно передать через инициализатор (например, в Storyboard/UIKit).

Классический пример — ViewController в iOS, который создается из Storyboard. Вы не можете изменить его init, поэтому используете свойство.

protocol LoggerProtocol {
    func log(message: String)
}

class ConsoleLogger: LoggerProtocol {
    func log(message: String) {
        print("LOG: \(message)")
    }
}

class MyViewController: UIViewController {
    // Опциональная зависимость
    var logger: LoggerProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()
        logger?.log(message: "View did load")
        // Если logger не установлен, ничего не произойдет
    }
}

// Настройка (например, в SceneDelegate или AppDelegate)
let viewController = MyViewController()
viewController.logger = ConsoleLogger()

Плюсы:

  • Гибкость: зависимость можно установить в любой момент.
  • Подходит для UIKit классов, создаваемых через Storyboard/XIB.
  • Легко реализовать для опциональных сервисов.

Минусы:

  • Объект может быть создан в невалидном состоянии (без зависимостей).
  • Требует внимательности: легко забыть установить зависимость.
  • Меньше "безопасности" на этапе компиляции.

Используйте Property Injection только тогда, когда Constructor Injection невозможен или неудобен.

3. DI-контейнер (Swinject): Организация на уровне приложения

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

Сначала установите Swinject через Swift Package Manager или CocoaPods. Затем настройте контейнер.

import Swinject

// 1. Определяем протоколы и классы
protocol AnalyticsServiceProtocol {
    func trackEvent(name: String)
}

class FirebaseAnalytics: AnalyticsServiceProtocol {
    func trackEvent(name: String) {
        print("Firebase: \(name)")
    }
}

protocol AuthServiceProtocol {
    func login()
}

class AuthService: AuthServiceProtocol {
    let analytics: AnalyticsServiceProtocol

    init(analytics: AnalyticsServiceProtocol) {
        self.analytics = analytics
    }

    func login() {
        analytics.trackEvent(name: "user_login")
        // Логика входа
    }
}

// 2. Регистрируем зависимости в контейнере
let container = Container()
container.register(AnalyticsServiceProtocol.self) { _ in FirebaseAnalytics() }
container.register(AuthServiceProtocol.self) { r in
    AuthService(analytics: r.resolve(AnalyticsServiceProtocol.self)!)
}

// 3. Разрешаем зависимости (resolve) там, где это нужно
let authService = container.resolve(AuthServiceProtocol.self)!
authService.login()
// Вывод: Firebase: user_login

Плюсы:

  • Централизованное управление всеми зависимостями.
  • Автоматическое разрешение цепочек зависимостей.
  • Упрощает масштабирование и рефакторинг.
  • Легко менять реализации (например, для тестирования).

Минусы:

  • Дополнительная зависимость от сторонней библиотеки.
  • Может скрывать реальную сложность графа зависимостей.
  • Ошибки конфигурации проявляются только в рантайме.

DI-контейнеры — это мощный инструмент для крупных проектов. Начинайте с ручного внедрения, а Swinject подключайте, когда почувствуете, что ручное управление начинает тормозить разработ

Блоги

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