重新组织链式可观察对象

Posted

技术标签:

【中文标题】重新组织链式可观察对象【英文标题】:Re-organizing chained observables 【发布时间】:2019-06-17 00:36:40 【问题描述】:

当通过table.rx.modelSelected 选择 tableviews 行时,我有大量的链式 Rx 可观察对象被触发。

我希望能够打破这个逻辑,因为我目前必须在 flatMapLatest 中执行业务逻辑,因为它是流程的“第 1 步”(感觉不对),我必须在后续的subscribe(“步骤2”)中执行更多的业务逻辑。这是我正在使用的代码:

locationsTable.rx.modelSelected(Location.self)
    .flatMapLatest  [weak self] location -> Observable<[JobState]?> in
        guard let hubs = self?.viewModel.userInfo.authorizedHubLocations else  return .empty() 
        guard let hub = hubs.first(where:  $0.locationId == location.id ) else  return .empty() 
        guard let hubToken = hub.hubToken else  return .empty() 

        // save data in db
        self?.databaseService.persistHub(hubResult: hub, location: location)

        // make network call for the 2nd step (the subscribe)
        let networkService = NetworkService(plugins: [AuthPlugin(token: hubToken)])
        return networkService.jobStates(locationId: location.id)
    
    .subscribe(onNext:  [weak self] jobState in
        if let jobState = jobState 
            self?.databaseService.persistJobStates(jobStates: jobState)
        
        NavigationService.renderScreenB()
    , onError:  error in
        Banner.showBanner(type: .error, title: "Whoops", message: "Something went wrong.")
    ).disposed(by: disposeBag)

此代码当前有效,但感觉很脏。任何有关如何清理它的建议将不胜感激。

【问题讨论】:

【参考方案1】:

您有几个独立且不同的逻辑和副作用位,您正试图将它们全部填充到单个 flatMap 中。我建议将它们分解成它们的组成部分。

另外,您的错误逻辑不正确。如果您的网络服务发出错误,您的“哎呀”横幅将显示,但它也会破坏您的链条,用户将无法选择其他位置。我下面的代码解决了这个问题。

下面的函数都是免费函数。由于它们不依赖于特定的视图控制器,因此它们可以独立使用和测试。另请注意,这些功能包含所有系统的逻辑和系统的逻辑。这使您可以测试没有副作用的逻辑并促进良好的架构。另请注意,它们返回 Drivers。您可以确定这些函数都不会发出会破坏链和视图控制器行为的错误。

/// Emits hubs that need to be persisted.
func hubPersist(location: ControlEvent<Location>, userInfo: UserInfo) -> Driver<(location: Location, hub: Hub)> 
    let hub = getHub(location: location, userInfo: userInfo)
        .asDriver(onErrorRecover:  _ in fatalError("no errors are possible") )
    return Driver.combineLatest(location.asDriver(), hub)  (location: $0, hub: $1) 


/// Values emitted by this function are used to make the network request.
func networkInfo(location: ControlEvent<Location>, userInfo: UserInfo) -> Driver<(NetworkService, Int)> 
    let hub = getHub(location: location, userInfo: userInfo)
    return Observable.combineLatest(hub, location.asObservable())
        .compactMap  (hub, location) -> (NetworkService, Int)? in
            guard let hubToken = hub.hubToken else  return nil 
            return (NetworkService(plugins: [AuthPlugin(token: hubToken)]), location.id)
        
        .asDriver(onErrorRecover:  _ in fatalError("no errors are possible") )


/// shared logic used by both of the above. Testing the above will test this by default.
func getHub(location: ControlEvent<Location>, userInfo: UserInfo) -> Observable<Hub> 
    return location
        .compactMap  location -> Hub? in
            let hubs = userInfo.authorizedHubLocations
            return hubs.first(where:  $0.locationId == location.id )
    

下面的函数是网络请求的包装器,它可以让错误更有用。

extension NetworkService 
    func getJobStates(locationId: Int) -> Driver<Result<[JobState], Error>> 
        return jobStates(locationId: locationId)
            .map  .success($0 ?? []) 
            .asDriver(onErrorRecover:  Driver.just(.failure($0)) )
    

这是您使用上述所有内容的视图控制器代码。它几乎完全由副作用组成。唯一的逻辑是几个守卫来检查网络请求的成功/失败。

func viewDidLoad() 
    super.viewDidLoad()

    hubPersist(location: locationsTable.rx.modelSelected(Location.self), userInfo: viewModel.userInfo)
        .drive(onNext:  [databaseService] location, hub in
            databaseService?.persistHub(hubResult: hub, location: location)
        )
        .disposed(by: disposeBag)

    let jobStates = networkInfo(location: locationsTable.rx.modelSelected(Location.self), userInfo: viewModel.userInfo)
        .flatMapLatest  networkService, locationId in
            return networkService.getJobStates(locationId: locationId)
        

    jobStates
        .drive(onNext:  [databaseService] jobStates in
            guard case .success(let state) = jobStates else  return 
            databaseService?.persistJobStates(jobStates: state)
        )
        .disposed(by: disposeBag)

    jobStates
        .drive(onNext:  jobStates in
            guard case .success = jobStates else  return 
            NavigationService.renderScreenB()
        )
        .disposed(by: disposeBag)

    jobStates
        .drive(onNext:  jobStates in
            guard case .failure = jobStates else  return 
            Banner.showBanner(type: .error, title: "Whoops", message: "Something went wrong.")
        )
        .disposed(by: disposeBag)

仅供参考,以上代码使用 Swift 5/RxSwift 5。

【讨论】:

这太棒了!我不确定如何测试我的原始方法,这使得 IMO 更容易

以上是关于重新组织链式可观察对象的主要内容,如果未能解决你的问题,请参考以下文章

ngFor正在根据可观察变量的变化重新渲染内容

RxJava入门

Rx scan(),无法从种子和另一个可观察对象生成可观察对象

当其他可观察对象为真时,从可观察对象中获取项目

如何创建一个表示其他两个可观察对象完成的可观察对象?

Knockout 订阅可观察的复杂对象的任何变化