Что такое Dependency Injection и как вы его реализуете в Swift?

Ответ

Dependency Injection (DI) — это паттерн проектирования, при котором зависимости объекта предоставляются извне, а не создаются внутри самого объекта. Это делает код более тестируемым, гибким и соответствующим принципу инверсии зависимостей (D из SOLID).

Три основных типа DI:

  1. Constructor Injection — зависимости передаются через инициализатор
  2. Property Injection — зависимости устанавливаются через свойства
  3. Method Injection — зависимости передаются как параметры метода

1. Constructor Injection (наиболее предпочтительный):

// Протокол для абстракции
protocol NetworkServiceProtocol {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}

protocol DatabaseServiceProtocol {
    func save(data: Data) throws
}

// Конкретные реализации
class NetworkService: NetworkServiceProtocol {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        // Реальная реализация сетевого запроса
    }
}

class DatabaseService: DatabaseServiceProtocol {
    func save(data: Data) throws {
        // Реальная реализация сохранения в БД
    }
}

// Класс, получающий зависимости через инициализатор
class DataManager {
    private let networkService: NetworkServiceProtocol
    private let databaseService: DatabaseServiceProtocol

    // Constructor Injection
    init(networkService: NetworkServiceProtocol,
         databaseService: DatabaseServiceProtocol) {
        self.networkService = networkService
        self.databaseService = databaseService
    }

    func fetchAndSave() {
        networkService.fetchData { [weak self] result in
            switch result {
            case .success(let data):
                try? self?.databaseService.save(data: data)
            case .failure(let error):
                print("Error: (error)")
            }
        }
    }
}

// Использование
let networkService = NetworkService()
let databaseService = DatabaseService()
let dataManager = DataManager(
    networkService: networkService,
    databaseService: databaseService
)

2. Property Injection (когда зависимости опциональны):

class AnalyticsManager {
    // Зависимость устанавливается после инициализации
    var analyticsService: AnalyticsServiceProtocol?

    func trackEvent(_ event: String) {
        analyticsService?.track(event: event)
    }
}

let manager = AnalyticsManager()
manager.analyticsService = FirebaseAnalyticsService() // Property Injection

3. Method Injection (для временных зависимостей):

class ImageProcessor {
    func process(image: UIImage, using filter: FilterProtocol) -> UIImage {
        return filter.apply(to: image) // Method Injection
    }
}

Реализация DI контейнера (ручная):

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

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

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

// Использование контейнера
let container = DIContainer()
container.register(NetworkServiceProtocol.self) {
    return NetworkService()
}
container.register(DatabaseServiceProtocol.self) {
    return DatabaseService()
}

let networkService = container.resolve(NetworkServiceProtocol.self)

Использование фреймворков (Swinject пример):

import Swinject

let container = Container()
container.register(NetworkServiceProtocol.self) { _ in
    return NetworkService()
}.inObjectScope(.container) // Singleton scope

container.register(DataManager.self) { resolver in
    let networkService = resolver.resolve(NetworkServiceProtocol.self)!
    let databaseService = DatabaseService()
    return DataManager(
        networkService: networkService,
        databaseService: databaseService
    )
}

let dataManager = container.resolve(DataManager.self)

Преимущества DI:

  • Тестируемость — легко подменять реальные сервисы моками
  • Гибкость — можно менять реализации без изменения клиентского кода
  • Чистая архитектура — соблюдение принципа инверсии зависимостей
  • Повторное использование — компоненты становятся более независимыми

Практические рекомендации:

  1. Всегда используйте протоколы для абстракции зависимостей
  2. Предпочитайте Constructor Injection там, где это возможно
  3. Для больших проектов используйте DI-фреймворки (Swinject, Needle)
  4. Избегайте Service Locator антипаттерна (глобальный доступ к сервисам)
  5. Документируйте зависимости в инициализаторах

Пример тестирования с DI:

class MockNetworkService: NetworkServiceProtocol {
    var shouldFail = false

    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        if shouldFail {
            completion(.failure(MockError.networkError))
        } else {
            completion(.success(Data()))
        }
    }
}

class DataManagerTests: XCTestCase {
    func testFetchAndSaveSuccess() {
        // Arrange
        let mockNetworkService = MockNetworkService()
        let mockDatabaseService = MockDatabaseService()
        let manager = DataManager(
            networkService: mockNetworkService,
            databaseService: mockDatabaseService
        )

        // Act
        manager.fetchAndSave()

        // Assert
        XCTAssertTrue(mockDatabaseService.saveWasCalled)
    }
}

Ответ 18+ 🔞

А, Dependency Injection, ёпта! Ну это ж та самая хуйня, когда ты не создаёшь свои зависимости внутри класса, как последний распиздяй, а тебе их, блядь, аккуратно подсовывают снаружи. Типа, не «я сам себе всё сделаю, я большой», а «на, дружок, держи готовенькое, не еби мозг».

Смотри, есть три основных способа, как эту дичь можно провернуть. И первый, самый правильный, блядь, — это через инициализатор запихнуть. Constructor Injection, называется. Вот смотри, как это выглядит, не обосрись со смеху:

protocol NetworkServiceProtocol {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}

protocol DatabaseServiceProtocol {
    func save(data: Data) throws
}

class DataManager {
    private let networkService: NetworkServiceProtocol
    private let databaseService: DatabaseServiceProtocol

    // Constructor Injection, сука! Всё засовывается сразу, при рождении.
    init(networkService: NetworkServiceProtocol,
         databaseService: DatabaseServiceProtocol) {
        self.networkService = networkService
        self.databaseService = databaseService
    }

    func fetchAndSave() {
        networkService.fetchData { [weak self] result in
            // ... тут логика, не суть важно
        }
    }
}

Видишь? DataManager нихуя не знает, какой там конкретно NetworkService прилетит — настоящий с запросами или муляж для тестов. Он просто требует: «Дайте мне что-то, что умеет fetchData, а остальное — похуй». Красота, блядь!

Второй способ — Property Injection. Это когда зависимость можно воткнуть уже после создания объекта, через свойство. Типа, «ой, забыл, держи сейчас». Используется, когда сервис опциональный, необязательный.

class AnalyticsManager {
    var analyticsService: AnalyticsServiceProtocol? // Смотри, опционал!

    func trackEvent(_ event: String) {
        analyticsService?.track(event: event) // А есть ли он вообще? Хуй его знает!
    }
}

Третий — Method Injection. Это когда ты передаёшь зависимость прямо в метод, как аргумент. Как одноразовый стаканчик, использовал и выкинул.

class ImageProcessor {
    func process(image: UIImage, using filter: FilterProtocol) -> UIImage {
        return filter.apply(to: image) // На, обработай этим фильтром и отъебись!
    }
}

А теперь, блядь, самое интересное — контейнеры. Это такие банки, где все твои сервисы живут, как солёные огурцы. Ты их там регистрируешь, а потом просто достаёшь, когда надо. Руками это делать, конечно, можно, но это как гвозди микроскопом забивать.

class DIContainer {
    private var services: [String: Any] = [:] // Вот тут вся наша братва сидит

    func register<Service>(_ type: Service.Type, factory: @escaping () -> Service) {
        let key = String(describing: type)
        services[key] = factory // Записали рецепт, как создать сервис
    }

    func resolve<Service>(_ type: Service.Type) -> Service? {
        let key = String(describing: type)
        guard let factory = services[key] as? () -> Service else {
            return nil // Искали-искали, нихуя не нашли
        }
        return factory() // Ага, вот он! Родили на свет божий.
    }
}

Нормальные пацаны для этого используют готовые фреймворки — Swinject, Needle. Там уже всё придумано, тебе только протоколы описать да сказать, что с чем связано.

А зачем этот весь цирк, спросишь? Да затем, сука!

  • Тестируемость — овердохуищная. Хочешь протестировать DataManager? Подсунул ему вместо настоящего сетевого сервиса — муляж, который всегда говорит «всё ок» или «всё пиздец». И проверяй, как он себя поведёт. Никаких реальных запросов, чистая изоляция!
  • Гибкость, блядь. Захотел сменить аналитику с Firebase на Яндекс.Метрику? Поменял одну строчку регистрации в контейнере — и весь проект использует новую либу. Не надо по всему коду ползать.
  • Архитектура чистая. Классы перестают быть всемогущими божествами, которые сами себе всё создают. Они становятся скромными работягами, которые просто требуют инструменты для работы. Это и есть тот самый принцип инверсии зависимостей — «D» из SOLID, если ты вдруг не в курсе этой ебалы.

Практические советы, чтоб не выстрелить себе в ногу:

  1. Протоколы — твои лучшие друзья. Без них DI превращается в хуйню. Все зависимости описывай протоколами.
  2. Constructor Injection — царь и бог. Используй его везде, где можно. Это самый явный и честный способ.
  3. Не выдумывай велосипед. На проекте больше трёх экранов — бери Swinject и не парься.
  4. Держись подальше от Service Locator. Это такой антипаттерн, когда у тебя есть глобальная помойка ServiceLocator.shared.get(.network). Это скрытая глобальная зависимость, хуже сифилиса. Избегай.

Вот и вся магия, ёпта. Вроде сложно, но как вникнешь — жить без этого не сможешь. Код становится прозрачным, как слёза младенца, и тестируемым, как лабораторная крыса. Главное — начать.