← Назад к вопросам

Как был устроен DI на прошлом месте работы?

1.0 Junior🔥 222 комментариев
#Soft Skills и карьера#Архитектура и паттерны

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Организация 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)!
    }
}

Ключевые принципы нашей реализации

  1. Constructor Injection как основной паттерн

    • Все обязательные зависимости передавались через инициализатор
    • Опциональные зависимости использовали property injection
  2. Протокол-ориентированный подход

    // Вместо конкретных классов всегда использовались протоколы
    protocol RepositoryProtocol {
        func save(_ item: Entity)
        func fetch() -> [Entity]
    }
    
    class CoreDataRepository: RepositoryProtocol {
        // Реализация
    }
    
    class TestRepository: RepositoryProtocol {
        // Mock для тестов
    }
    
  3. Модульные контейнеры для фич

    • Каждый feature-модуль имел собственный FeatureDIContainer
    • Родительские зависимости передавались через инициализатор
    • Это позволяло изолировать модули и переиспользовать их
  4. Ленивая инициализация сложных объектов

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

Преимущества нашего подхода

  • Слабая связанность компонентов системы
  • Упрощенное тестирование за счет легкого мокирования
  • Читаемость кода - зависимости явно объявлены
  • Гибкость - возможность легко менять реализации
  • Безопасность - проверка зависимостей на этапе компиляции через протоколы
  • Модульность - возможность выделять фичи в отдельные пакеты

Проблемы и их решения

Мы столкнулись с несколькими вызовами:

  1. Циклические зависимости - решены через протоколы и lazy injection
  2. Инициализация в правильном порядке - использовали фазы регистрации
  3. Производительность - кэшировали часто используемые сервисы
  4. Миграция legacy кода - постепенное внедрение через адаптеры

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

Как был устроен DI на прошлом месте работы? | PrepBro