Эта статья - финальный аккорд в нашей трилогии об архитектуре. Мы уже научились наводить порядок внутри экрана с помощью 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()    
  }
}

Практические советы и наблюдения

  1. Протоколы для Фабрик: Всегда закрывайте фабрику протоколом. Это позволит вам написать MockFactory для тестов и проверять, что координатор вообще пытается создать нужный экран.

  2. Много фабрик лучше, чем одна: Если проект огромный, не делайте одну GiantFactory. Разделите их по фичам: AuthFactory, ProfileFactory, ChatFactory.

  3. SwiftUI и DI: В SwiftUI есть EnvironmentObject, который часто путают с DI. Помните, что EnvironmentObject - это, по сути, глобальная переменная внутри дерева вью. Для сложной бизнес-логики я всё равно рекомендую использовать классический DI через инициализаторы ваших ObservableObject (ViewModel).

Комбинация MVVM, Coordinator и Factory - это золотой стандарт для масштабируемых iOS-приложений. Это дает вам:

  • Тестируемость: Каждый компонент изолирован.

  • Гибкость: Вы можете менять UI или логику, не переписывая навигацию.

  • Порядок: У каждого объекта есть одна четкая ответственность.

Да, поначалу кажется, что вы пишете много «лишнего» кода. Но как только вам прилетит задача «а давайте в этом флоу заменим второй экран на новый, но только для пользователей из США», вы скажете себе спасибо за то, что выбрали этот путь.