Эта статья - финальный аккорд в нашей трилогии об архитектуре. Мы уже научились наводить порядок внутри экрана с помощью MVVM и управлять потоками переходов через Coordinator. Но остался один неудобный вопрос: кто создаст все эти зависимости? Если ваш Координатор превратился в свалку из десятка сервисов, которые он просто пробрасывает дальше, значит, пришло время внедрить Factory. Вы узнаете, как разделить создание объектов и управление ими, почему глобальные DI-контейнеры - это яд замедленного действия, и как построить систему, где каждый компонент получает только то, что ему нужно, не зная лишнего.
Когда мы только начинаем внедрять Coordinator, всё кажется прекрасным. Но проходит пара месяцев, проект растет, и внезапно оказывается, что ваш MainCoordinator в инициализаторе принимает NetworkService, DatabaseService, AnalyticsService, ImageLoader и еще пяток «очень нужных» штук. Причем сам Координатор эти сервисы не использует - он просто держит их у себя, чтобы потом передать во ViewModel.
Поздравляю, вы построили «трубопровод зависимостей». Это плохая практика. Координатор должен знать, когда ��оказать экран, но он не обязан знать, как собрать этот экран по кирпичикам. Для этого у нас есть паттерн Factory.
Проблема: перегруженный координатор
Давайте посмотрим правде в глаза: если ваш код выглядит вот так, у вас проблемы с декомпозицией:
Swift
// ТАК ДЕЛАТЬ НЕ НАДО final class ProductCoordinator: Coordinator { let networkService: NetworkProtocol let authService: AuthProtocol let cache: CacheProtocol // ... еще 5 сервисов func start() { // Координатор знает слишком много о деталях сборки let vm = ProductViewModel(network: networkService, auth: authService) let vc = ProductViewController(viewModel: vm) navigationController.pushViewController(vc, animated: true) } }
Здесь Координатор жестко связан с конкретными реализациями ViewModel и ViewController. Если вы захотите изменить способ создания ProductViewModel (например, добавить новый логгер), вам придется лезть в Координатор. А теперь представьте, что таких экранов десять.
Решение: контейнер с зависимостями и фабрика
Идея проста: мы создаем отдельный объект (или группу объектов), чья единственная задача - знать, как собрать экран. Я предпочитаю разделять это на две части: Dependency Container (хранилище сервисов) и Module Factory (создатель экранов).
1. Dependency Container: источник правды
Это долгоживущий объект, который хранит в себе синглтоны или конфигурации сервисов. Но важно: он не должен быть глобальным синглтоном Container.shared. Это ловушка, превращающая ваш код в нетестируемое месиво (Service Locator anti-pattern).
Контейнер должен передаваться явно или инжектиться в Фабрику.
Swift
protocol DependencyContainerProtocol { var networkService: NetworkProtocol { get } var databaseService: DatabaseProtocol { get } } final class AppDependencyContainer: DependencyContainerProtocol { // Ленивая инициализация, чтобы не создавать всё сразу lazy var networkService: NetworkProtocol = NetworkService() lazy var databaseService: DatabaseProtocol = DatabaseService() }
2. Фабрика модулей: строитель
Фабрика - это «цех», который штампует готовые модули. Она знает о существовании Контейнера и умеет доставать из него нужные запчасти.
Swift
protocol ModuleFactoryProtocol { func makeProductListModule(coordinator: ProductListCoordinator) -> UIViewController func makeProductDetailModule(product: Product) -> UIViewController } final class ModuleFactory: ModuleFactoryProtocol { private let container: DependencyContainerProtocol init(container: DependencyContainerProtocol) { self.container = container } func makeProductListModule(coordinator: ProductListCoordinator) -> UIViewController { let viewModel = ProductListViewModel( network: container.networkService, coordinator: coordinator ) return ProductListViewController(viewModel: viewModel) } }
Интеграция фабрики в координатор
Теперь наш Координатор становится по-настоящему легким. Он просто просит фабрику: «Дай мне контроллер для списка продуктов, вот тебе ссылка на меня для обратной связи».
Swift
final class ProductListCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] var navigationController: UINavigationController private let factory: ModuleFactoryProtocol init(navigationController: UINavigationController, factory: ModuleFactoryProtocol) { self.navigationController = navigationController self.factory = factory } func start() { let viewController = factory.makeProductListModule(coordinator: self) navigationController.pushViewController(viewController, animated: true) } }
Посмотрите, какая чистота! Координатор теперь не знает о NetworkService, он не знает о ViewModel. Если завтра вы решите заменить MVVM на VIPER для одного конкретного экрана - вы просто измените реализацию в Фабрике. Координатор об этом даже не узнает.
Почему не Service Locator?
Часто возникает искушение сделать ServiceLocator.shared.get(NetworkProtocol.self). Это кажется удобным: не нужно ничего пробрасывать в инициализаторы.
Но я категорически против этого в больших проектах.
Скрытые зависимости: Глядя на
initконтроллера, вы не видите, что ему нужна сеть. Это вскроется только в рантайме, если вы забудете зарегистрировать сервис.Сложность тестирования: Вам придется сбрасывать состояние глобального синглтона перед каждым тестом, что превращает параллельный запуск тестов в ад.
Явное прокидывание через Фабрику (Constructor Injection) - это честный способ. Да, кода чуть больше, зато вы спите спокойно.
Там, где всё начинается
Где создается вся эта махина? В Composition Root. Обычно это SceneDelegate или AppDelegate. Это единственное место в приложении, которое знает обо всех компонентах и связывает их воедино.
Swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var appCoordinator: AppCoordinator? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let container = AppDependencyContainer() let factory = ModuleFactory(container: container) let nav = UINavigationController() appCoordinator = AppCoordinator(navigationController: nav, factory: factory) appCoordinator?.start() let window = UIWindow(windowScene: windowScene) window.rootViewController = nav self.window = window window.makeKeyAndVisible() } }
Практические советы и наблюдения
Протоколы для Фабрик: Всегда закрывайте фабрику протоколом. Это позволит вам написать
MockFactoryдля тестов и проверять, что координатор вообще пытается создать нужный экран.Много фабрик лучше, чем одна: Если проект огромный, не делайте одну
GiantFactory. Разделите их по фичам:AuthFactory,ProfileFactory,ChatFactory.SwiftUI и DI: В SwiftUI есть
EnvironmentObject, который часто путают с DI. Помните, чтоEnvironmentObject- это, по сути, глобальная переменная внутри дерева вью. Для сложной бизнес-логики я всё равно рекомендую использовать классический DI через инициализаторы вашихObservableObject(ViewModel).
Комбинация MVVM, Coordinator и Factory - это золотой стандарт для масштабируемых iOS-приложений. Это дает вам:
Тестируемость: Каждый компонент изолирован.
Гибкость: Вы можете менять UI или логику, не переписывая навигацию.
Порядок: У каждого объекта есть одна четкая ответственность.
Да, поначалу кажется, что вы пишете много «лишнего» кода. Но как только вам прилетит задача «а давайте в этом флоу заменим второй экран на новый, но только для пользователей из США», вы скажете себе спасибо за то, что выбрали этот путь.
