我正在努力将我的一些View模型移植到(粗略的)有限状态机中,因为我的UI倾向于很好地适应该模式(Mealy / Moore,不关心这个问题的目的)。此外,当完成状态良好 - 状态机真正清理测试 - 因为它们禁止某些测试排列发生。
我当前的视图模型使用RxSwift(和RxKotlin - 取决于应用程序),底层用例(数据库调用,网络调用等)也使用Rx(因此我需要留在该生态系统中)。
我发现Rx很棒,状态机很棒 - > Rx +状态机看起来有点像哈希做任何不平凡的事情。例如,我知道我可以使用.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
}
}
}
我已经尝试让.scan
操作符接受一个Observable累加器,但是这段代码的结果是状态机订阅或运行了太多次。我猜是因为它在正在积累的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
})
})
但是,从一个私有状态变量拉到一个可观察的流,并更新它 - 好吧,不仅是它丑陋,我觉得我只是等待被并发错误击中。
编辑:根据Sereja Bogolubov的反馈 - 我添加了一个接力并提出这个代码 - 仍然不是很好,但到了那里。
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
中从状态转换的结果更新...这仍然感觉它可能导致并发问题,但不知道还有什么工作。
不,您不必完全同步以维持任意复杂状态。是的,如果没有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 ...
})
);
请注意,更复杂的案例(如竞争条件有风险)可能需要至少与combineLatest和approp一样复杂的事情。 Scheduler
到位了。
我认为Elm的系统在这里可以派上用场。在Elm中,你传递给系统的reducer不仅仅返回状态,它还返回一个“命令”,在我们的例子中它将是一个Observable<Event>
(不是RxSwift.Event,而是你的事件枚举。)这个命令不是' t存储在扫描状态,但是它在扫描之外订阅并且其输出被反馈到扫描中(通过某种类型的主题。)需要取消的任务将观察当前状态并基于启动和停止操作国家。
RxSwift生态系统中有几个库可以帮助简化这些事情。两个主要的是,ReactorKit和RxFeedback。还有其他几个......
有关我所说的简单示例,请查看this gist。这种系统允许您的Moore机器在进入可能导致0..n新输入事件的状态时触发动作。