【问题标题】:How do I properly organize this Rx-based reactive state machine?如何正确组织这个基于 Rx 的反应状态机?
【发布时间】:2021-09-26 15:52:15
【问题描述】:

请注意:尽管我在示例中使用 Swift-lang,但我鼓励您尝试帮助我,即使您了解 RxJava 和 rxjs 或任何其他 Rx 实现。

问题:我有一个命令式编写的状态机。我想使用 Rx 进行响应式重写,但我正在努力想出正确的方法。

当前的命令式系统如下所示:

var state: State
var metaState: MetaState

func updateMetaStateIfNeeded() {
    if Bool.random() {
        metaState = MetaState()
    }
}

func tick() -> State {
    updateMetaStateIfNeeded()

    let newState = State(currentState: state, metaState: metaState)
    self.state = newState
    return newState
}

到目前为止我想出了什么:

let tick = PublishSubject<Void>()
let metaState = BehaviorSubject<MetaState>()
let state = BehaviorSubject<State>()

let tickAfterMetaStateUpdate = PublishSubject<Void>()

tick -> withLatestFrom metaState -> map to new metaState if update needed or the old one -> put result into metaState AND tickWithUpdatedMetaState

tickAfterMetaStateUpdate -> withLatestFrom state AND metaState -> map State(currentState: state, metaState: metaState) -> put result into state

(The users of this API subscribe to state subject, which will be updated every tick)

我对我提出的这个结果不满意。我想知道是否可以使用scanreduce 重写它,这样就没有使用 Subjects 并且它是一个普通的 Observable。

@Daniel T. 回答后更新:

哇,这正是正确的解决方案!我已经准备好了分析:

  • 有2个状态机,一个用于metaState更新,一个用于state更新
  • 他们每个人都有自己的开始状态,我们不需要指定
  • metaState 的输入字母是Void,只是一个勾号。对于state,它是第一个状态机的metaState 输出

所以有了这个理解,我用 2 次扫描重写了系统:

let state = tick
    .scan(MetaState()) { currentMetaState, void in
        if Bool.random() {
            return MetaState()
        }
        return currentMetaState
    }
    .scan(State()) { currentState, currentMetaState in
        State(currentState: state, metaState: metaState)
    }

WINRAR! Mind_blast.jpg!实际上,事后看来,这似乎很明显...... :) 谢谢,谢谢,谢谢!

【问题讨论】:

标签: rxjs rx-java reactive-programming rx-swift frp


【解决方案1】:

嗯...是的,scan 是实现state machine 的首选运算符。为此,您需要一组有限的状态(通常实现为结构,但也可以是枚举)、开始状态(通常使用 State 结构上的默认构造函数实现)、输入字母表(通常实现为枚举,通常称为 Event、Command 或 Action 的一些变体)和一个转换函数(传递给 scan 的闭包。)

所以这里真正的问题是改变状态的命令/动作是什么?大多数情况下,动作是由用户动作产生的,但你的问题并没有真正说明什么是动作,输入字母是什么......

遗憾的是,问题中没有足够的信息来提供更多细节。

-- 更新--

抱歉,您的更新不正确。传递给scan 的闭包应该是纯的,而第一个肯定不是。如果MetaState.init() 是纯事件,则Bool.random() 不是。

您应该将不纯的闭包传递给 Observable 的唯一情况是当您创建新的 Observable、使用最终结果时,或者在 do 内。

一个简单的修复你所拥有的看起来像这样:

let state = tick
    .flatMap { // this closure is an Observable factory. You can do impure things here.
        Observable.just(Bool.random() ? MetaState() : nil)
    }
    .scan(State()) { currentState, metaState in // this closure should be pure.
        if let metaState = metaState {
            return State(currentState: currentState, metaState: metaState)
        }
        else {
            return currentState
        }
    }

这肯定是一个小改动,但它更清楚地显示了代码的意图,并明确了哪些可以进行单元测试,哪些不能进行单元测试。

【讨论】:

  • 很棒的答案,帮了我很多!这完全解决了我的问题,我非常感谢!我已经编辑了我的问题,并更新了我如何使用扫描解决问题!
  • 我已根据您的更新更新了我的答案。