依赖注入(六):架构黄金标准:为何选择Coordinator,以及如何用好它

290 阅读5分钟

在前面的分享中,我们已经建立了对“显式依赖注入”的深刻认同。现在,我们面临最后一个,也是最关键的架构决策:如何组织我们应用的导航逻辑?我们是应该改良现有的Router模式,还是全面转向Coordinator模式?

我将首先清晰地辨析CoordinatorRouter的本质区别,以阐明我们做出选择的理由。然后,我将结合MVVM和现代并发框架,为大家呈现一套完整的Coordinator模式最佳实践。

第一部分:正本清源 - Coordinator vs. Router

在很多讨论中,CoordinatorRouter经常被混用,但它们在设计哲学和实现细节上有着天壤之别。

特性Router (URL-based 或 Protocol-based)Coordinator
核心隐喻全局“电话总机”或“URL调度中心”专职“旅行团导游”
核心职责响应一个标识符(URL或协议),触发一个未知的跳转管理一个完整、具象的业务流程(Use Case)
调用方式Router.open("app://profile?id=123")profileCoordinator.start(userId: "123")
参数传递通常是弱类型(字符串),难以传递复杂对象强类型,可以通过构造函数或方法传递任何对象
依赖管理隐式(黑盒):调用方不知道目标页面需要什么显式(白盒):Coordinator负责为页面注入其所有依赖
导航控制全局或分散,导航逻辑(push/present)与业务流分离内聚,由Coordinator自身完全控制其流程内的导航
生命周期通常是全局单例有自己的生命周期,可以随业务流创建和销毁

结论:为何必须选择Coordinator?

Router模式的根本问题在于它是一个服务定位器(Service Locator)。它鼓励了“主动查询”的行为,隐藏了依赖关系,牺牲了类型安全和可测试性。任何试图改良Router的努力,都只是在为一个有缺陷的地基做表面装修。

Coordinator模式是一次架构思想的升维。它天生就与依赖注入思想完美契合:

  • 它自身通过DI被创建和配置。
  • 它作为DI的执行者,为它所管理的MVVM栈注入依赖。
  • 它将导航逻辑(“How”)与业务意图(“What”)清晰地分离开来。

因此,我们的决策是明确且坚定的:在应用内部导航中,全面采用Coordinator模式,彻底摒弃基于服务定位器的Router模式。 仅在需要响应外部事件(推送、H5)时,可以保留一个极轻量的URLDispatcher,其唯一作用是解析URL并启动一个根Coordinator


第二部分:最佳实践 - Coordinator + MVVM + 现代并发

现在,让我们进入实战环节。以下是一套完整的、值得在所有新项目中推行的最佳实践。

  • DI容器 (Swinject): 超级工厂。只在组合根中配置,负责创建一切。
  • Coordinator: 交通指挥。负责业务流、依赖组装和导航。
  • MVVM (View - ViewModel): 场景呈现者。ViewModel处理逻辑和状态,View负责渲染。

2. 核心原则:清晰的通信边界

  • Coordinator -> ViewModel: 通过构造函数注入依赖(如Service)。
  • ViewModel -> Coordinator: 绝对不能反向持有Coordinator的引用! ViewModel通过响应式信号(Combine/AsyncStream)或闭包向外发送导航“意图”。
  • ViewModel <-> View: 通过@StateObject/@ObservedObject@Published属性进行双向绑定。

3. 完整示例:一个支持回调的“商品选择”流程

想象一个场景:在创建订单页面,需要弹出一个商品选择页面,选择完商品后,需要将商品ID回调给创建订单页面。

步骤1:定义Coordinator协议和父子关系

protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }
    func start()
}

// 方便地管理子Coordinator
extension Coordinator {
    func addChild(_ childCoordinator: Coordinator) {
        childCoordinators.append(childCoordinator)
    }
    
    func removeChild(_ childCoordinator: Coordinator) {
        childCoordinators = childCoordinators.filter { $0 !== childCoordinator }
    }
}

步骤2:实现ProductSelectionCoordinator

import Combine

class ProductSelectionCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    private let resolver: Resolver

    // 使用Combine的Subject来传递回调结果,类型安全
    let selectionResult = PassthroughSubject<String, Never>()
    private var cancellables = Set<AnyCancellable>()

    init(resolver: Resolver, navigationController: UINavigationController) {
        self.resolver = resolver
        self.navigationController = navigationController
    }

    func start() {
        let viewModel = resolver.resolve(ProductSelectionViewModel.self)!
        let viewController = ProductSelectionViewController(viewModel: viewModel)
        
        // 监听ViewModel的输出
        viewModel.didSelectProduct
            .sink { [weak self] productId in
                // 1. 将结果通过自己的Subject发射出去
                self?.selectionResult.send(productId)
            }
            .store(in: &cancellables)
        
        viewModel.didCancel
            .sink { [weak self] in
                // 2. 空转结果,表示取消
                self?.selectionResult.send(completion: .finished)
            }
            .store(in: &cancellables)

        // 通常以模态形式弹出
        navigationController.present(UINavigationController(rootViewController: viewController), animated: true)
    }
}

步骤3:实现ProductSelectionViewModel

import Combine

class ProductSelectionViewModel: ObservableObject {
    private let productService: ProductServicing
    
    // 输出给Coordinator的信号
    let didSelectProduct = PassthroughSubject<String, Never>()
    let didCancel = PassthroughSubject<Void, Never>()
    
    // 输出给View的状态
    @Published var products: [Product] = []
    
    init(productService: ProductServicing) {
        self.productService = productService
    }
    
    // 供View调用的方法
    func selectProduct(at index: Int) {
        let productId = products[index].id
        didSelectProduct.send(productId)
    }
    
    func cancelButtonTapped() {
        didCancel.send()
    }

    @MainActor
    func fetchProducts() async {
        self.products = await productService.fetchAll()
    }
}

步骤4:在父Coordinator (CreateOrderCoordinator) 中使用它

class CreateOrderCoordinator: Coordinator {
    // ...
    func showProductSelection() {
        let selectionCoordinator = resolver.resolve(ProductSelectionCoordinator.self, argument: self.navigationController)!
        addChild(selectionCoordinator)
        
        selectionCoordinator.selectionResult
            .sink(receiveCompletion: { [weak self] _ in
                // 无论成功或取消,流程结束,都移除子Coordinator
                self?.removeChild(selectionCoordinator)
                self?.navigationController.dismiss(animated: true)
            }, receiveValue: { [weak self] productId in
                // 成功获取到商品ID,更新自己的ViewModel
                self?.viewModel.update(with: productId)
            })
            .store(in: &cancellables)
            
        selectionCoordinator.start()
    }
    // ...
}

4. 易错点与最终建议

  • 生命周期管理是关键: 必须通过addChildremoveChild来正确管理Coordinator的生命周期,否则将导致内存泄漏。
  • 通信必须单向: ViewModel绝不能知道Coordinator的存在。通信永远是ViewModel -> Coordinator的单向信号。
  • DI容器的纯洁性: ViewModelView中绝对不能出现container.resolve的代码。依赖必须在初始化时注入。

总结:一套值得信赖的架构

通过将DI容器CoordinatorMVVM三者有机结合,我们建立了一个分层清晰、职责单一、高度可测的黄金架构。它解决了传统MVC和Router模式的种种弊病,为我们构建复杂、可维护的大型应用提供了坚实的基础。

在第七篇中,我将会介绍一个 MVVMC 的实际使用示例,敬请期待。