如何在DI+Coordinators+MVP架构下同步所有注入对象的账户实时数据
解决账户数据同步问题的可行方案
我来给你几个适配你当前DI+Coordinator+MVP架构的可行方案,既能解决数据同步问题,又能保留现有架构的优势,避免你之前提到的那些弊端:
方案1:用响应式流包装账户对象(推荐,适合Swift生态)
如果你的项目已经引入了Combine(iOS 13+)或者RxSwift,这个方案是最优雅的。核心思路是用Observable/Subject作为单一数据源,让所有组件订阅这个流来获取最新账户数据,而不是直接注入FIRAccount实例。
具体实现:
- 在AppCoordinator中创建一个
CurrentValueSubject(Combine)或者BehaviorSubject(RxSwift),用来持有并推送最新的账户数据:
// Combine版本 import Combine class AppCoordinator { private let accountSubject: CurrentValueSubject<FIRAccount, Never> private var cancellables = Set<AnyCancellable>() init(initialAccount: FIRAccount, ...) { self.accountSubject = CurrentValueSubject(initialAccount) // 监听Firestore账户变化,收到更新时推送新数据 firestoreRepository.listenToAccountUpdates { [weak self] newAccount in self?.accountSubject.send(newAccount) } } }
- 注入时,给子协调器、Presenter传递这个流的抽象(比如
AnyPublisher),而不是直接传FIRAccount:
// 在TabBarCoordinator的初始化中 self.coord1 = Coord1( firestoreRepository, firebaseCloudStorageService, geoLocationService, alertHandlerService, accountPublisher: accountSubject.eraseToAnyPublisher(), // 传递流 delegate: self )
- 在Presenter等组件中订阅这个流,自动接收最新数据:
class EditProfilePresenter { private let accountPublisher: AnyPublisher<FIRAccount, Never> private var cancellables = Set<AnyCancellable>() init(accountPublisher: AnyPublisher<FIRAccount, Never>, ...) { self.accountPublisher = accountPublisher // 订阅流,收到新数据时更新本地状态和UI accountPublisher .receive(on: DispatchQueue.main) .sink { [weak self] newAccount in self?.updateLocalAccountState(newAccount) self?.view?.refreshProfileUI(with: newAccount) } .store(in: &cancellables) } }
优势:
- 真正的单一数据源,所有组件共享同一个数据流,更新自动同步
- 保留了DI的优势,可测试性强(测试时可以用
TestPublisher模拟数据) - 不需要手动管理通知或监听器,响应式框架自动处理订阅/取消逻辑
方案2:创建专门的AccountManager服务(职责清晰,无额外依赖)
如果不想引入响应式框架,可以创建一个只负责账户状态管理的服务,把账户数据的监听、存储、同步逻辑都封装在这里,作为单一数据源供所有组件依赖。
具体实现:
- 定义协议,明确AccountManager的职责:
protocol AccountManagerProtocol { var currentAccount: FIRAccount { get } // 注册更新监听,返回可取消的对象(用Combine或者自定义闭包) func addAccountUpdateListener(_ handler: @escaping (FIRAccount) -> Void) -> AnyCancellable }
- 实现AccountManager类,内部维护最新账户实例,并监听Firestore的更新:
class AccountManager: AccountManagerProtocol { private var currentAccount: FIRAccount private let firestoreRepository: FirestoreRepositoryProtocol private let updateHandlers = NSHashTable<AnyObject>.weakObjects() // 弱引用避免内存泄漏 private let queue = DispatchQueue(label: "com.yourapp.AccountManager.queue") init(initialAccount: FIRAccount, firestoreRepository: FirestoreRepositoryProtocol) { self.currentAccount = initialAccount self.firestoreRepository = firestoreRepository // 监听Firestore更新 firestoreRepository.listenToAccountUpdates { [weak self] newAccount in self?.handleAccountUpdate(newAccount) } } private func handleAccountUpdate(_ newAccount: FIRAccount) { queue.sync { self.currentAccount = newAccount // 通知所有注册的监听者 self.updateHandlers.allObjects.forEach { handler in (handler as? (FIRAccount) -> Void)?(newAccount) } } } func addAccountUpdateListener(_ handler: @escaping (FIRAccount) -> Void) -> AnyCancellable { let wrapper = HandlerWrapper(handler: handler) updateHandlers.add(wrapper) return AnyCancellable { [weak self] in self?.updateHandlers.remove(wrapper) } } // 用来包装闭包的辅助类,因为NSHashTable不能直接存闭包 private class HandlerWrapper: NSObject { let handler: (FIRAccount) -> Void init(handler: @escaping (FIRAccount) -> Void) { self.handler = handler } } }
- 在AppCoordinator中初始化AccountManager,然后注入给所有需要账户数据的组件:
// AppCoordinator初始化 let accountManager = AccountManager(initialAccount: fetchedAccount, firestoreRepository: firestoreRepository) // 注入给TabBarCoordinator let tabBarCoordinator = TabBarCoordinator( navigationController, firestoreRepository, ..., accountManager: accountManager )
- 组件可以直接获取
currentAccount,或者注册监听获取更新:
class Coord1 { private let accountManager: AccountManagerProtocol private var cancellables = Set<AnyCancellable>() init(accountManager: AccountManagerProtocol, ...) { self.accountManager = accountManager // 注册监听 accountManager.addAccountUpdateListener { [weak self] newAccount in self?.updateComponentState(with: newAccount) }.store(in: &cancellables) } }
优势:
- 遵循单一职责原则:AccountManager只负责账户状态的管理,不混杂Firestore操作逻辑
- 单一数据源,所有组件依赖同一个AccountManager,数据同步一致
- 保留DI的优势,可测试性强(Mock AccountManagerProtocol即可)
- 扩展性好,后续可以添加本地缓存、权限校验等账户相关逻辑
方案3:将FIRAccount改为引用类型并线程安全更新(最小改动)
如果你的FIRAccount现在是值类型(struct),可以改成引用类型(class),然后在AppCoordinator中维护唯一的实例,注入给所有组件。当收到Firestore更新时,直接更新该实例的属性,而不是替换整个对象,这样所有持有该实例的组件都会看到最新值。
具体实现:
- 将FIRAccount改为class,并添加线程安全的属性访问:
class FIRAccount: NSObject { private let queue = DispatchQueue(label: "com.yourapp.FIRAccount.queue") // 私有存储属性,通过队列保证线程安全 private var _username: String private var _email: String // 公开的访问属性,用队列同步读写 var username: String { get { queue.sync { _username } } set { queue.sync { _username = newValue } } } var email: String { get { queue.sync { _email } } set { queue.sync { _email = newValue } } } init(username: String, email: String, ...) { self._username = username self._email = email super.init() } }
- 在AppCoordinator中维护唯一的FIRAccount实例,收到更新时直接修改属性:
class AppCoordinator { private let currentAccount: FIRAccount init(initialAccount: FIRAccount, ...) { self.currentAccount = initialAccount firestoreRepository.listenToAccountUpdates { [weak self] newAccount in // 直接更新现有实例的属性 self?.currentAccount.username = newAccount.username self?.currentAccount.email = newAccount.email // 其他属性同理 } } }
- 注入时直接传递这个实例给所有组件,组件持有该实例的引用,自动看到最新属性值。
优势:
- 改动最小,不需要引入新的框架或服务
- 保留DI模式,组件依然通过构造注入获取账户对象
注意点:
- 必须保证线程安全,避免多线程同时读写导致的数据不一致
- 如果FIRAccount的结构复杂,逐个更新属性会比较繁琐
方案对比与选择建议
- 如果你已经在用Combine/RxSwift:优先选方案1,响应式的方式最优雅,自动同步,代码简洁。
- 如果你不想引入响应式框架:选方案2,职责清晰,扩展性好,是最符合SOLID原则的方案。
- 如果你想最小化改动,且FIRAccount结构不复杂:可以选方案3,但要注意线程安全问题。
这三个方案都避免了你之前提到的单例紧耦合、多监听器重复读取、通知注册遗漏等问题,同时保留了DI、Coordinator和MVP模式的优势。
内容的提问来源于stack exchange,提问作者Alex Marchant
相关产品推荐
相关产品推荐

