Ответ
Dependency Injection (DI) — это паттерн проектирования, при котором зависимости объекта предоставляются извне, а не создаются внутри самого объекта. Это делает код более тестируемым, гибким и соответствующим принципу инверсии зависимостей (D из SOLID).
Три основных типа DI:
- Constructor Injection — зависимости передаются через инициализатор
- Property Injection — зависимости устанавливаются через свойства
- 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:
- Тестируемость — легко подменять реальные сервисы моками
- Гибкость — можно менять реализации без изменения клиентского кода
- Чистая архитектура — соблюдение принципа инверсии зависимостей
- Повторное использование — компоненты становятся более независимыми
Практические рекомендации:
- Всегда используйте протоколы для абстракции зависимостей
- Предпочитайте Constructor Injection там, где это возможно
- Для больших проектов используйте DI-фреймворки (Swinject, Needle)
- Избегайте Service Locator антипаттерна (глобальный доступ к сервисам)
- Документируйте зависимости в инициализаторах
Пример тестирования с 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)
}
}