问题
I have a pretty hefty chunk of chained Rx observables that are fired when a tableviews row is selected via table.rx.modelSelected
.
I'd like to be able to break this logic up, because I'm currently having to execute business logic in flatMapLatest
, because it's "Step 1" to the process (which feels wrong), and I have to execute more business logic in the subsequent subscribe
("Step 2"). Here's the code I'm using:
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)
This code currently works, but it feels dirty. Any advice on how to clean this up would be greatly appreciated.
回答1:
You have several separate and distinct bits of logic and side-effects and you are trying to stuff them all into a single flatMap. I suggest breaking them up into their component parts.
Also, your error logic isn't correct. If your network service emits an error your "Whoops" banner will display, but it will also break your chain and the user won't be able to select a different location. My code below fixes this problem.
The functions below are all free functions. Since they are not tied to a specific view controller, they can be used and tested independently. Also notice that these functions encompass all the logic and only the logic of the system. This allows you to test the logic free of side-effects and promotes good architecture. Also notice that they return Driver
s. You can be sure that none of these functions will emit an error which would break the chain and the view controller's behavior.
/// 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 })
}
}
The function below is a wrapper around your network request that makes errors more usable.
extension NetworkService {
func getJobStates(locationId: Int) -> Driver<Result<[JobState], Error>> {
return jobStates(locationId: locationId)
.map { .success($0 ?? []) }
.asDriver(onErrorRecover: { Driver.just(.failure($0)) })
}
}
Here is your view controller code using all of the above. It consists almost exclusively of side effects. The only logic are a couple of guards to check for success/failure of the network request.
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)
}
FYI, the above code uses Swift 5/RxSwift 5.
来源:https://stackoverflow.com/questions/56623583/re-organizing-chained-observables