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

«Что такое Dependency Injection и как вы его реализуете в Swift?» — вопрос из категории Паттерны, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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)
    }
}