


在不破坏行为的前提下构建和修改事件驱动状态机
在构建或修改状态机库时使用。AI 会先把一个事件处理完再处理下一个,出错时也不跳过收尾回调,保证线程和子状态机安全,先写测试并在同步和异步引擎上运行,并让代码、文档和模式保持一致。
要使用这个AI规则
## 决定状态机的各部分逻辑放在哪个模块
更改代码前先梳理职责,使行为留在掌握所需知识的组件中。
### 划定清晰边界
- 将状态和事件定义、转换组合、回调分派、签名适配、执行引擎、注册表、集成和序列化彼此分离。
- 使分派和签名适配等通用基础设施独立于状态机领域。
- 让每个模块、类和函数都只有一个明确的变更理由。
- 与其为了方便而扩展无关模块,不如优先采用具有明确接口的小型专用模块。
### 有意识地放置逻辑
- 应用Information Expert(信息专家)原则:在已具备必要知识的位置计算结果,然后返回或发出该结果的信号,而不是在其他位置重新计算。
- 应用Law of Demeter(迪米特法则):向协作方传递其所需的具体值,而不是该值所属的父对象。
- 应用Interface Segregation(接口隔离)原则:如果辅助程序不需要完整对象,就只接收范围较窄的字段或协议。
- 重构时保留公开行为,并在移动逻辑前找出能够刻画该行为的测试。
## 处理完一个事件后再处理下一个
在两个明确不同的层级上建立事件处理模型,实施时不得将其合并。
### 区分处理层级
- 微步按照规定顺序,以原子方式执行一组已选转换:前置回调、退出回调、转换操作、进入回调和后置回调。
- 宏步处理一个外部事件,并重复执行微步,直至状态机达到稳定配置。
- 只有在没有启用任何无事件转换且内部队列为空时,配置才是稳定的。
### 明确队列与事件语义
- 从外部发送的事件进入外部队列,并等待当前宏步结束。
- 内部引发的事件进入内部队列,并在当前宏步内先于后续外部事件运行。
- 未赋值的转换声明可以表示一种无事件转换,在其守卫条件变为真时触发。
- 已赋值的转换会创建命名事件;如果根据命名约定派生别名,明确指定的标识符优先。
- 将队列顺序、无事件转换的推进和稳定配置下的终止分别作为独立行为进行测试。
## 把错误当作事件处理,不跳过收尾回调
明确在哪里拦截错误,以及发生失败后仍要运行哪些生命周期回调。
### 设置错误边界
- 针对每种状态机类型,明确将错误捕获为事件的策略;面向状态图的变体与基础变体可以有意采用相反的默认设置。
- 在操作块层级捕获错误,而不是将整个微步视为一个捕获边界;当生命周期有所保证时,无论成功还是失败都要运行终结回调。
- 将捕获的执行错误作为内部事件分派,使状态图能够通过已声明的转换进行处理。
- 对处理内部执行错误事件期间引发的错误予以抑制或终止,防止形成递归失败循环。
### 保持回调之间的区别
- 将通用生命周期回调与事件特定回调及状态特定回调分开。
- 名称与错误相关的回调不会自动成为通用回调;按约定调用前,必须存在相应的错误转换。
- 如果转换操作在错误事件处理期间停用局部错误拦截,应将这条规则与始终使用自身错误处理程序的进入块和退出块规则区分开。
- 分别测试成功、普通失败、错误处理期间的失败以及终结顺序。
## 修改队列或锁时,保证线程和子状态机安全
更改队列、锁或子状态机管理时,保持线程安全和调用路由方面的保证不变。
### 维持安全的并发处理
- 对于一个状态机实例,同一时间最多只允许一个线程执行转换。
- 支持并发发送方时,使用线程安全的优先队列。
- 除非重新设计并证明并发契约,否则不得用普通列表或非线程安全的双端队列替换线程安全队列。
- 使用协调一致的发送线程和屏障,对真实竞争情况进行压力测试。
- 在阻塞线程测试中,使用带明确超时且可中断的事件等待,不要使用可能在清理期间遗留线程的固定休眠。
### 精确管理被调用的子状态机
- 只有当子调用既已终止又已取消时,才将其移除。
- 只要所属状态仍处于活动状态,就应让已终止但未取消的子状态机保持可路由。
- 如果子状态机的构建在处理循环内造成阻塞,应在循环继续前,通过早期生命周期监听器取得对子状态机的引用。
- 通过调用管理器和子状态机处理程序路由以子状态机为目标的事件,不得绕过生命周期控制。
- 将取消、自然终止、保留路由和延迟事件交付作为不同情况分别测试。
## 在项目固定的开发环境中运行测试,并测量分支覆盖率
通过项目固定的环境和任务运行器执行安装、测试、质量检查和文档工作。
### 安全执行测试
- 为完整测试套件设置明确的超时,以暴露遗留线程或无限循环。
- 当测试包含有意等待或备用超时时,对完整套件使用受支持的并行执行方式。
- 如果限制路径的调用会遗漏模块或文档的doctest,应从项目根目录调用规范测试套件。
- 对超过既定基准的运行进行调查,不要在未诊断原因的情况下反复重跑。
- 如果全局详细调试日志会改变性能测量,只在有针对性的调查中启用详细调试日志。
- 对大量输出只捕获一次,然后分析保存的结果,不要重复运行同一个高成本测试套件。
### 测量预期的覆盖率
- 需要时明确启用覆盖率收集,然后为受影响的模块生成缺失行报告或机器可读报告;仅选择报告格式可能不会启用收集。
- 提交前不仅要验证行覆盖率,还要验证分支覆盖率。
## 先写会失败的测试,并在同步和异步引擎上运行
将测试视为实现要求,而不是代码编写完成后的清理工作。
### 遵循测试驱动循环
1. 规划时识别新的分支、边界情况、错误路径和并发风险。
2. 针对缺失或不正确的行为,添加一个会失败的专项测试。
3. 实现能让该测试通过的最小行为变更。
4. 运行专项测试、受影响模块的覆盖率检查以及所需的更广泛检查。
5. 只有在行为和覆盖率得到保护后才进行重构。
### 复用测试基础设施
- 使用共享的参数化运行器,针对同步和异步引擎运行同一状态图。
- 让运行器抽象引擎启动和事件交付,而不是重复编写测试类或添加空操作监听器。
- 对临时路径、环境变更和可变状态使用夹具;不得硬编码路径或依赖可变的全局变量。
- 只要防御性分支可达,就应使用测试覆盖;只有在确认该分支不可能发生时,才能将其排除在覆盖率之外。
- 通过标准提交检查持续强制执行所需的分支覆盖率阈值。
## 运行代码检查,让解析器和模式保持一致
对实现、示例、生成的契约和开发者文档应用同一套一致的质量体系。
### 强制执行代码约定
- 通过项目的规范工具链运行格式化程序、代码检查程序、导入排序程序、类型检查程序和测试。
- 优先使用已排序的顶层导入;只有在需要打破循环依赖时才使用延迟导入。
- 遵循项目的命名和docstring约定,记录隐藏的语义,而不是重复显而易见的名称或类型。
- 文档生成器要求时,将属性docstring直接放在属性下方;仅为不明显的逻辑保留行内注释。
### 测试文档与声明式格式
- 在可行时,将文档示例制作成可执行的doctest。
- 将需要真实并发的示例放入单元测试,并从文档中引用。
- 每当声明式词汇发生变化时,都要保持解析器、运行时行为和已发布模式同步。
- 将解析器接受的键集合视为事实依据,并测试它们是否与对应的模式属性一致。
- 根据模式验证每个结构化示例和夹具,使解析器与模式发生偏差时测试套件会失败。
## 不绕过提交钩子、不提交密钥,完成变更
使用代码库的贡献流程,保持可追溯性、生成产物和安全边界。
### 完成质量关卡
- 以代码库指定的开发分支为目标,并遵循其提交消息约定。
- 项目有此偏好时,使用签名提交。
- 绝不得绕过提交钩子或停用必需的检查。
- 如果钩子重新生成或修改文件,应审查这些结果并暂存,然后再次提交,直至所有钩子均通过。
- 审查最终差异,并确认实现、测试、文档和生成的契约彼此一致。
### 保护信任边界
- 绝不得提交机密、凭据或环境文件。
- 在系统边界验证不可信数据,并明确内部不变量。
- 不得仅仅为了让变更通过,就削弱队列安全、错误循环防护、模式验证或覆盖率强制要求。
- 报告任何无法运行的必需检查;没有直接结果时,不得声称已经完成。
