Я работаю над портированием некоторых из моих моделей представления в (грубые) конечные автоматы, поскольку мой пользовательский интерфейс имеет тенденцию достаточно хорошо соответствовать этому шаблону (Мили / Мур, не волнует цель этого вопроса).Кроме того, когда все сделано хорошо, конечные автоматы действительно очищают тестирование, поскольку они запрещают выполнение определенных тестовых перестановок.
В моих текущих моделях представления используется 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
из результата перехода состояния ... Это все еще похоже на то, что это может вызвать проблему параллелизма, но не уверен, чтоиначе будет работать.