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

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

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