Как сохранить состояние в Rx без функции сканирования - PullRequest
2 голосов
/ 09 марта 2019

Я работаю над портированием некоторых из моих моделей представления в (грубые) конечные автоматы, поскольку мой пользовательский интерфейс имеет тенденцию достаточно хорошо соответствовать этому шаблону (Мили / Мур, не волнует цель этого вопроса).Кроме того, когда все сделано хорошо, конечные автоматы действительно очищают тестирование, поскольку они запрещают выполнение определенных тестовых перестановок.

В моих текущих моделях представления используется RxSwift (и RxKotlin - в зависимости от приложения) и базовые сценарии использования.(вызовы из базы данных, сетевые вызовы и т. д.) также используют Rx (поэтому я должен оставаться в этой экосистеме).

Я обнаружил, что Rx великолепен, State Machines великолепны -> Rx +State Machines, кажется, немного хэш, чтобы сделать что-нибудь нетривиальное.Например, я знаю, что могу использовать оператор .scan для сохранения некоторого состояния, ЕСЛИ мой конечный автомат был полностью синхронным (например, что-то примерно такое в Swift):

enum Event {
    case event1
    case event2
    case event3
}

enum State {
    case state1
    case state2
    case state3

    func on(event: Event) -> State {
        switch (self, event) {
        case (.state1, .event1):
            // Do something
            return .state2

        case (.state2, .event2):
            // Do something
            return .state3

        default:
            return self // (or nil, or something)
        }
    }
}

func foo() -> Observable<State> {
    let events = Observable<Event>.of(.event1, .event2, .event3)
    return events.scan(State.state1) { (currentState, event) -> State in
        return currentState.on(event)
    }
}

Но что можетЯ делаю, если возвращение из моей функции State.on является Observable (например, сетевой вызов или что-то, что занимает много времени, что уже происходит в Rx)?

enum State {
    case notLoggedIn
    case loggingIn
    case loggedIn
    case error

    func on(event: Event) -> Observable<State> {
        switch (self, event) {
        case (.notLoggedIn, .event1):
            return api.login(credentials)
                .map({ (isLoggedIn) -> State in
                    if isLoggedIn {
                        return .loggedIn
                    }
                    return .error
                })
                .startWith(.loggingIn)

        ... other code ...

        default:
            return self
        }
    }
}

Я пытался сделать *Оператор 1013 * получает аккумулятор Observable, но в результате этого кода конечный автомат подписывается или запускается слишком много раз.Я думаю, потому что он работает на каждом состоянии в наблюдаемой, которая накапливается.

return events.scan(Observable.just(State.state1)) { (currentState, event) -> Observable<State> in
    currentState.flatMap({ (innerState) -> Observable<State> in
        return innerState.on(event: event)
    })
}.flatMap { (states) -> Observable<State> in
    return states
}

Думаю, если бы мне удалось аккуратно вернуть переменную state, простейшая реализация могла бы выглядеть так:

return events.flatMapLatest({ (event) -> Observable<State> in
    return self.state.on(event: event)
        .do(onNext: { (state) in
            self.state = state
        })
})

Но, извлекая из частногопеременная состояния в наблюдаемый поток и ее обновление - ну, это не только некрасиво, но я чувствую, что просто жду, чтобы попасть под ошибку параллелизма.


Редактировать: на основе отзывов отСережа Боголюбов - Я добавил Реле и придумал этот код - все еще не очень хорошо, но добираюсь.

let relay = BehaviorRelay<State>(value: .initial)
...

func transition(from state: State, on event: Event) -> Observable<State> {
    switch (state, event) {
    case (.notLoggedIn, .event1):
        return api.login(credentials)
            .map({ (isLoggedIn) -> State in
                if isLoggedIn {
                    return .loggedIn
                }
                return .error
            })
            .startWith(.loggingIn)

    ... other code ...

    default:
        return self
    }
}

return events.withLatestFrom(relay.asObservable(), resultSelector: { (event, state) -> Observable<State> in
    return self.transition(from: state, on: event)
        .do(onNext: { (state) in
            self.relay.accept(state)
        })
}).flatMap({ (states) -> Observable<State> in
    return states
})

Реле (или тема воспроизведения или что-то еще) обновляется в doOnNext из результата перехода состояния ... Это все еще похоже на то, что это может вызвать проблему параллелизма, но не уверен, чтоиначе будет работать.

Ответы [ 2 ]

1 голос
/ 10 марта 2019

Я думаю, что система Элма может пригодиться здесь.В Elm редуктор, который вы передаете в систему, не просто возвращает состояние, он также возвращает «команду», которая в нашем случае будет Observable<Event> (не RxSwift.Event, а ваше перечисление Event). Эта командане сохраняется в состоянии сканирования, а подписывается на него вне пределов сканирования, и его выходные данные возвращаются в сканирование (через какой-либо объект). Задачи, требующие отмены, будут отслеживать текущее состояние и запускать и останавливать операциюна основе состояния.

В экосистеме RxSwift есть несколько библиотек, которые помогают упростить подобные вещи.Два основных из них: ReactorKit и RxFeedback .И есть несколько других ...

Для простого примера того, о чем я говорю, посмотрите этот гист .Система такого типа позволяет вашему компьютеру Moore запускать действие при входе в состояние, которое потенциально может вызвать 0..n новых событий ввода.

1 голос
/ 09 марта 2019

Нет, вам не нужно быть полностью синхронизированными, чтобы поддерживать произвольное сложное состояние. Да, есть способы достичь необходимого поведения без scan. Как насчет withLatestFrom , где other - ваше текущее состояние (т. Е. Отдельный Observable<MyState>, но вам потребуется ReplaySubject<MyState> под капотом).

Дайте мне знать, если вам нужно больше деталей.


Подтверждение концепции, javascript:

const source = range(0, 10);
const state = new ReplaySubject(1);
const example = source.pipe(
  withLatestFrom(state), // that's the way you read actual state
  map(([n, currentState]) => {
    state.next(n); // that's the way you change the state
    return ...
  })
);

Имейте в виду, что в более сложных случаях (например, в условиях гонки) может потребоваться что-то, по крайней мере, такое же сложное, как CombatLatest и Approp. Scheduler на месте.

...