事件溯源(Event Sourcing):如何合并分歧状态?求最佳实践
状态转换方案点评与最佳实践建议
场景说明
- 事件类型:
A触发、B触发、Ping触发,事件序列示例:A,A,A,B,Ping - 目标状态:
InA、InB、PingMissing - 状态转换规则:
- 事件序列中无
Ping→ 最终状态为PingMissing - 序列中出现
A→ 优先对应InA(需结合规则1判断) - 序列中出现
B→ 优先对应InB(需结合规则1判断) - 仅存在
Ping事件 → 最终状态为InA
- 事件序列中无
三种实现思路点评
思路1:新增伪事件整合至单一函数
- 核心逻辑:引入
PingMissing这类伪事件,将所有状态转换逻辑封装进单个函数f(s,e)->s - 优缺点:
- 优点:单函数入口,逻辑集中,初期理解成本低
- 缺点:伪事件属于额外抽象,会增加事件类型复杂度;规则迭代时,单函数内的分支会快速膨胀,维护难度上升
思路2:双独立转换函数+结果合并
- 核心逻辑:拆分状态为两个独立维度(如「A/B状态」和「Ping接收状态」),用两个转换函数分别处理,最后合并维度得到最终状态
- 优缺点:
- 优点:职责清晰,每个函数只处理单一维度的状态变化;单个函数逻辑简单,易测试、易维护;新增规则时只需调整合并逻辑或新增维度函数
- 缺点:需要额外定义中间状态类型,初期有少量类型定义成本;若维度间存在强耦合,合并逻辑会变得复杂
思路3:单函数维护元组状态
- 核心逻辑:用元组(如
(ABState, PingState))承载多维度状态,单个转换函数同时更新元组内所有状态,最后从元组推导最终状态 - 优缺点:
- 优点:无需拆分函数,状态更新的原子性强;元组能直观体现状态的多维度属性
- 缺点:单个函数需处理所有维度的状态更新逻辑,维度增多时函数分支会繁琐;元组可读性不如命名状态类型,调试时不够直观
最佳实践建议
针对当前场景,思路2是最优选择,理由如下:
- 当前规则核心是两个正交维度:「最后一次有效A/B事件」和「是否收到Ping」,完全适合拆分处理
- 扩展性强:后续新增「C触发」事件时,只需修改
State1和f1函数,无需改动Ping相关逻辑;新增「Ping超时」规则时,只需调整State2和f2的逻辑 - 测试成本低:可分别对
f1和f2做单元测试,验证单个维度的状态转换正确性,再测试合并逻辑,比单函数测试更高效
附思路2的F#实现代码:
type Event = | A | B | Ping type State1 = | InA | InB type State2 = | PingReceived | PingMissing type StateCombined = | InA' | InB' | PingMissing' let f1 s e :State1 = match s,e with | _, A -> InA | _, B -> InB | _, _ -> s let f2 s e :State2 = match s,e with | _, Ping -> PingReceived | _, _ -> s let fCombined events = let finalState1 = events |> Seq.fold f1 InA let finalState2 = events |> Seq.fold f2 PingMissing match finalState1, finalState2 with | _, PingMissing -> PingMissing' | InA, _ -> InA' | InB, _ -> InB' // 测试用例 fCombined [A;A;A;B] // 输出:PingMissing' fCombined [A;A;A;B;Ping] // 输出:InB'
内容的提问来源于stack exchange,提问作者jim108dev
相关产品推荐
相关产品推荐

