Как обеспечить слабую связанность и независимость между классами без DI фреймворков
Что проверяют
Понимаете ли вы, что внедрение зависимостей это приём, а не библиотека, и умеете ли собрать граф объектов руками. Следом обычно спрашивают, где у вас создаются зависимости и как вы подменяете их в тестах.
Как отвечать
В Swift всё нужное есть без сторонних фреймворков:
- протоколы на границах:
ProfileViewModelзависит отProfileService, а не от конкретного сетевого класса. В тестах подставляется заглушка; - внедрение через
init: зависимости приходят в инициализатор и хранятся вlet. Объект нельзя создать недоукомплектованным, в отличие отvar service: Service!; - композиционный корень: одно место, где собирается граф, например
SceneDelegateили структураAppв SwiftUI. Там же живёт сборщик вродеAppDependencies, который создаёт сервисы и передаёт их вниз; - замыкания вместо протокола: если зависимость это одна функция, хватит
let loadUser: (Int) async throws -> User; - координаторы: навигация уходит из контроллеров, и экраны перестают знать друг о друге;
- SwiftUI: сквозные зависимости кладут в
Environmentсо своим ключом, в свежих SDK его объявляют макросом@Entry; - модули Swift Package Manager: протоколы в одном пакете, реализация в другом. Запрещённую зависимость просто не получится импортировать.
В Swift 6 добавляется ещё одна граница: зависимости, которые пересекают акторы, должны быть Sendable или изолированы на @MainActor.
Где спотыкаются
Везде используют .shared. Синглтон это жёсткая связь, спрятанная от сигнатуры: по init не видно, от чего класс зависит, и подменить его в тесте нельзя.
Предлагают глобальную шину событий или NotificationCenter как способ развязать классы. Связь не исчезает, а становится неявной, а подписки через сильные замыкания держат объекты в памяти.
Делают delegate сильной ссылкой. Контроллер и делегат держат друг друга, получается цикл удержания, и экран не освобождается.