Как был устроен DI на прошлом месте работы?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Организация Dependency Injection на проекте
На моём предыдущем месте работы мы использовали комбинированный подход DI, который эволюционировал от ручной инъекции к использованию сторонней библиотеки с сохранением гибкости архитектуры. Основным инструментом стала библиотека Swinject, которую мы дополнили собственными абстракциями для улучшения тестируемости и модульности.
Архитектурные слои и внедрение зависимостей
Система была организована в три основных слоя с четкими правилами инъекции:
// Пример структуры модуля
protocol ServiceProtocol {
func fetchData() -> AnyPublisher<Data, Error>
}
class DataService: ServiceProtocol {
private let networkClient: NetworkClientProtocol
private let storage: StorageProtocol
// Constructor Injection - основной способ
init(networkClient: NetworkClientProtocol, storage: StorageProtocol) {
self.networkClient = networkClient
self.storage = storage
}
func fetchData() -> AnyPublisher<Data, Error> {
// Реализация
}
}
Контейнеры зависимостей
Мы создали иерархию контейнеров для разных сценариев:
// Основной AppContainer
final class AppDIContainer {
static let shared = AppDIContainer()
private let container: Container
private init() {
container = Container()
registerDependencies()
}
private func registerDependencies() {
// Сиглтоны
container.register(NetworkManagerProtocol.self) { _ in
NetworkManager.shared
}.inObjectScope(.container)
// Новые инстансы для каждого разрешения
container.register(DataParserProtocol.self) { _ in
JSONDataParser()
}.inObjectScope(.transient)
// Графовые зависимости
container.register(ViewModelProtocol.self) { resolver in
MainViewModel(
service: resolver.resolve(ServiceProtocol.self)!,
analytics: resolver.resolve(AnalyticsProtocol.self)!
)
}
}
func resolve<T>(_ type: T.Type) -> T {
container.resolve(type)!
}
}
Ключевые принципы нашей реализации
-
Constructor Injection как основной паттерн
- Все обязательные зависимости передавались через инициализатор
- Опциональные зависимости использовали property injection
-
Протокол-ориентированный подход
// Вместо конкретных классов всегда использовались протоколы protocol RepositoryProtocol { func save(_ item: Entity) func fetch() -> [Entity] } class CoreDataRepository: RepositoryProtocol { // Реализация } class TestRepository: RepositoryProtocol { // Mock для тестов } -
Модульные контейнеры для фич
- Каждый feature-модуль имел собственный
FeatureDIContainer - Родительские зависимости передавались через инициализатор
- Это позволяло изолировать модули и переиспользовать их
- Каждый feature-модуль имел собственный
-
Ленивая инициализация сложных объектов
container.register(ComplexServiceProtocol.self) { resolver in ComplexService( network: resolver.resolve(NetworkProtocol.self)!, cache: resolver.resolve(CacheProtocol.self)!, config: resolver.resolve(ConfigurationProtocol.self)! ) }.initCompleted { resolver, service in // Дополнительная настройка после инъекции service.delegate = resolver.resolve(ServiceDelegateProtocol.self) }
Управление жизненным циклом объектов
Мы использовали различные object scopes для оптимизации:
.container- для синглтонов (NetworkManager, Analytics).graph- для объектов, живущих в пределах одного графа зависимостей.transient- для объектов, которые должны создаваться заново каждый раз
Интеграция с UI слоем
Для ViewController'ов мы использовали property wrapper:
@propertyWrapper
struct Inject<Service> {
private var service: Service?
var wrappedValue: Service {
mutating get {
if service == nil {
service = AppDIContainer.shared.resolve(Service.self)
}
return service!
}
}
}
class MainViewController: UIViewController {
@Inject var viewModel: MainViewModelProtocol
@Inject var analytics: AnalyticsProtocol
// Остальная реализация
}
Тестирование и мокирование
Наша система DI позволяла легко подменять реализации для тестов:
class FeatureTests: XCTestCase {
private var testContainer: Container!
override func setUp() {
super.setUp()
testContainer = Container()
// Регистрируем mock-зависимости
testContainer.register(NetworkProtocol.self) { _ in
MockNetworkService()
}
// Используем testContainer в тестах
let viewModel = testContainer.resolve(ViewModelProtocol.self)!
}
}
Преимущества нашего подхода
- Слабая связанность компонентов системы
- Упрощенное тестирование за счет легкого мокирования
- Читаемость кода - зависимости явно объявлены
- Гибкость - возможность легко менять реализации
- Безопасность - проверка зависимостей на этапе компиляции через протоколы
- Модульность - возможность выделять фичи в отдельные пакеты
Проблемы и их решения
Мы столкнулись с несколькими вызовами:
- Циклические зависимости - решены через протоколы и lazy injection
- Инициализация в правильном порядке - использовали фазы регистрации
- Производительность - кэшировали часто используемые сервисы
- Миграция legacy кода - постепенное внедрение через адаптеры
Наш опыт показал, что правильно организованная DI система значительно упрощает поддержку кодовой базы, особенно в больших проектах с командой из 10+ разработчиков. Ключевым инсайтом стало понимание, что DI — это не только про внедрение зависимостей, но и про организацию архитектуры и коммуникации между компонентами приложения.