Реорганизация цепных наблюдаемых - PullRequest
2 голосов
/ 17 июня 2019

У меня есть довольно большой кусок цепочек наблюдаемых Rx, которые запускаются, когда строка табличных представлений выбирается с помощью table.rx.modelSelected.

Я хотел бы иметь возможность разбить эту логику, потому что в настоящее время мне приходится выполнять бизнес-логику в 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 Ответ

1 голос
/ 17 июня 2019

У вас есть несколько отдельных битов логики и побочных эффектов, и вы пытаетесь объединить их все в одну плоскую карту. Я предлагаю разбить их на составные части.

Кроме того, ваша логика ошибок неверна. Если ваша сетевая служба выдает ошибку, будет отображаться ваш баннер «К сожалению», но это также разорвет вашу цепочку, и пользователь не сможет выбрать другое местоположение. Мой код ниже решает эту проблему.

Все функции ниже являются свободными функциями. Поскольку они не привязаны к конкретному контроллеру представления, их можно использовать и тестировать независимо. Также обратите внимание, что эти функции охватывают всю логику и только логику системы. Это позволяет вам тестировать логику без побочных эффектов и продвигает хорошую архитектуру. Также обратите внимание, что они возвращают Driver с. Вы можете быть уверены, что ни одна из этих функций не выдаст ошибку, которая разорвет цепочку и поведение контроллера представления.

/// 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)
}

FYI, приведенный выше код использует Swift 5 / RxSwift 5.

...