ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

条件工作流避免烂尾:用类型系统建模分支判断

条件工作流避免烂尾:用类型系统建模分支判断 2. 先看一个具体的痛点条件判断是工作流里最容易“烂尾”的部分如果说工作流持久化关心的是状态机怎么推进那么条件工作流关心的就是状态之间怎么选择路径。很多团队在最初几周里把工作流的节点、状态、事件都设计得清清楚楚到了条件分支就开始放飞自我if (approved.equals(task.getResult()) task.getAmount() 10000) { workflow.moveTo(financeReview); } else if (rejected.equals(task.getResult())) { workflow.moveTo(end); }这段代码在业务量小的时候完全没问题。但随着规则增多你会看到条件散落在各 Service 里没有一个统一入口条件写成了字符串常量甚至直接写死approved改成枚举后发现有一处if没改equals永远返回 false线上流程静默走到了错误分支测试的时候只能靠人肉构造各种状态组合漏掉一个分支就出事故。这就是典型的无类型条件写法——条件判断的逻辑是对的但条件本身没有被类型系统约束起来。字符串、数字、布尔值散落在代码各处编译器无法帮你检查重构的时候全靠肉眼。而今天我们讨论的“条件工作流的有类型写法”就是把条件本身建模成类型。它解决的问题不是“条件表达式怎么写更短”而是让编译器帮你检查分支是否完备让条件从“散落的 if 字符串”变成“可以集中定义、复用、测试的数据结构”让业务规则的变更落到有限几个类型上而不是满代码库找字符串。这篇文章会从一个订单审批流程的实际例子出发分别用 TypeScript 和 Java 展示两种有类型写法并给出完整代码、运行演示和工程建议。读完你可以直接把这些思路落到自己的项目里不需要引入任何重量级框架。3. 无类型条件写法到底差在哪里在进入有类型写法之前先把无类型写法的三个主要问题讲透。很多团队不是不想改而是没意识到这些问题有多隐蔽。3.1 问题一条件逻辑散落不可复用假设同一个业务里有三种流程订单审核、退款审核、工单派发。它们都依赖相同的判断条件当前金额是否超过某阈值、当前角色是否有审批权。无类型写法的典型结果是这样// 订单审核服务 if (order.amount 10000 currentUser.hasRole(FINANCE)) { ... } // 退款审核服务 if (refund.amount 10000 currentUser.hasRole(FINANCE)) { ... } // 工单派发服务 if (ticket.priority HIGH currentUser.hasRole(OPS)) { ... }金额阈值、角色名、比较逻辑全部散落在不同 Service 中。当你把阈值从 10000 改成 5000 的时候得全局搜索10000找出所有相关位置然后祈祷没有遗漏。3.2 问题二字符串比较导致编译期失明无类型写法的核心问题在于条件所依赖的判断标准是字符串或魔法数字而这些值在编译期没有任何检查。approved拼成approve编译器不会报错FINANCE改成FINANCE_DEPT所有写死的旧字符串不会自动更新if (task.status 3)里的3是什么状态半年后没人记得。这些错误只能在运行时暴露甚至在特定组合下才触发排错成本非常高。3.3 问题三分支不可穷尽工作流里最容易出事的不是主路径而是“未穷尽”的分支。一个if-else if-else结构开发者很可能只处理了正常业务能遇到的情况却漏掉了某些边界组合。无类型写法下分支穷尽性完全靠人的自觉。有人觉得反正会有默认 else不写也行——结果线上流程走到某个从未设想的组合时默认分支把数据推到了错误状态最后要靠数据库工单来补。这三个问题叠加起来就是很多人说的“工作流代码跑着跑着就成了一团乱麻”。它和技术水平关系不大而是模型选错了——你用字符串和 if 去表达本来应该用类型系统去表达的东西。4. 有类型写法的核心思想有类型写法并不是一种特定框架而是一套设计思路。它的本质是把“条件”当作一等公民用类型系统来约束条件的定义、组合和判断。这里先建立几个基础概念后面才能在代码里看清楚。4.1 什么是“条件即数据”无类型写法里条件和业务逻辑混在一起判断的过程就是执行代码的过程。有类型写法的第一步是把一个条件表示成一份可结构化的数据。例如type Condition | { kind: amount; operator: gt | lt; value: number } | { kind: role; role: FINANCE | OPS | ADMIN } | { kind: and; left: Condition; right: Condition };这里Condition就是一个类型。它描述了一个条件“长什么样”而不是直接去判断真假。真正判断的时候可以写一个解释器来执行它function evaluate(cond: Condition, ctx: Context): boolean { switch (cond.kind) { case amount: return cond.operator gt ? ctx.amount cond.value : ctx.amount cond.value; case role: return ctx.roles.includes(cond.role); case and: return evaluate(cond.left, ctx) evaluate(cond.right, ctx); } }这样的好处是条件可以被序列化成 JSON存入数据库或配置文件条件可以被集中管理、测试、日志输出条件的结构在编译期就是确定的不容易拼错。4.2 什么是“分支即类型”比“条件即数据”更进一步是用**判别联合Discriminated Union或密封接口Sealed Interface**来表达一条流程的所有可能分支。无类型写法里流程走向是一个隐式概念靠 if 的走向体现。有类型写法里流程走向是一个显式类型type OrderFlowStep | { kind: start } | { kind: autoApprove } | { kind: needManagerReview; reason: string } | { kind: needFinanceReview; amount: number } | { kind: done; result: approved | rejected };拿到这样的类型后编译器可以帮你检查switch分支是否覆盖了所有可能的流程节点你不能再写一个没有定义的流程节点每个节点自带数据不再靠外部MapString, Object传参。4.3 有类型写法的三个层次层次做法解决的问题第一层用枚举替代字符串拼写错误、散落常量第二层用代数数据类型联合类型/密封接口建模条件和分支分支不可穷尽、状态无结构第三层用解释器模式统一执行用类型守卫收敛判断逻辑条件不可测试、逻辑散落大部分团队能做好第一层就够了但遇到复杂流程第二层和第三层带来的收益会非常明显。4.4 与规则引擎的关系有些读者会问这不就是规则引擎吗确实Condition建模成数据之后和轻量规则引擎的思路非常接近。区别在于规则引擎如 Drools一般是一套完整的 DSL 和配套运行时适合团队有专职规则维护者有类型写法不需要额外工具链直接利用语言自带的类型系统适合大部分业务代码场景。如果你只是因为几个if太乱而想引入规则引擎其实有点大材小用。先尝试把条件建模成类型往往就够了。5. 环境准备与前置条件后面的示例代码可以在您本机的常规开发环境里直接运行。为了不限制版本这里只说明我演示时用的基础环境实际版本以您的项目为准。环境项说明TypeScript 示例Node.js TypeScript 编译器不需要额外框架Java 示例JDK 17 及以上因为用到 sealed interface不需要 Spring 或其他框架操作系统Windows / macOS / Linux 均可运行方式命令行直接执行或通过 IDE 运行 main 方法TypeScript 的安装和初始化npm install -g typescript mkdir typed-workflow cd typed-workflow npm init -y npm install typescript ts-node --save-dev npx tsc --init --target es2020 --strictJava 示例不需要额外的依赖管理直接写一个.java文件用java命令运行即可。6. 核心流程拆解这部分是整篇文章的主干。我们以一个通用场景为例订单审批流程。流程逻辑如下订单金额小于 1000自动通过走autoApprove金额在 1000 到 10000 之间需要部门经理审批走managerReview金额大于 10000需要部门经理和财务双重审批走financeReview审批结果为拒绝直接结束走done。这个流程足够简单但又能体现条件分支、多步流转、类型建模三个要素。6.1 第一步定义流程节点类型流程节点是整个工作流的核心数据类型。在 TypeScript 里用判别联合定义export type OrderStatus | { kind: CREATED } | { kind: AUTO_APPROVED } | { kind: MANAGER_REVIEW; manager: string } | { kind: FINANCE_REVIEW; manager: string; amount: number } | { kind: DONE; result: APPROVED | REJECTED };每一个节点带了自己的数据。比如FINANCE_REVIEW节点记录了审批经理是谁、金额是多少这让每一步流转都能自解释。6.2 第二步定义条件和判断函数条件本身也是一个类型。这里用Condition来表达“金额大于某值”和“角色匹配”export type Condition | { kind: amountGt; value: number } | { kind: amountLt; value: number } | { kind: hasRole; role: string } | { kind: and; left: Condition; right: Condition } | { kind: or; left: Condition; right: Condition }; export function evaluateCondition(cond: Condition, ctx: OrderContext): boolean { switch (cond.kind) { case amountGt: return ctx.amount cond.value; case amountLt: return ctx.amount cond.value; case hasRole: return ctx.userRoles.includes(cond.role); case and: return evaluateCondition(cond.left, ctx) evaluateCondition(cond.right, ctx); case or: return evaluateCondition(cond.left, ctx) || evaluateCondition(cond.right, ctx); default: // 使用了穷尽性检查 const _exhaustive: never cond; throw new Error(Unknown condition kind: ${JSON.stringify(_exhaustive)}); } }注意最后的never分支。TypeScript 会在这里做穷尽性检查如果Condition将来新增了一种类型而switch没有处理编译阶段就会报错而不是运行到线上才出问题。6.3 第三步定义流转函数流转函数接收当前状态和上下文返回下一个状态。这里就能体现“类型驱动流转”的价值export interface OrderContext { orderId: string; amount: number; userRoles: string[]; } export function transit(status: OrderStatus, ctx: OrderContext): OrderStatus { switch (status.kind) { case CREATED: if (ctx.amount 1000) { return { kind: AUTO_APPROVED }; } if (ctx.amount 1000 ctx.amount 10000) { if (ctx.userRoles.includes(MANAGER)) { return { kind: MANAGER_REVIEW, manager: managerA }; } return { kind: DONE, result: REJECTED }; } if (ctx.amount 10000) { if (ctx.userRoles.includes(MANAGER) ctx.userRoles.includes(FINANCE)) { return { kind: FINANCE_REVIEW, manager: managerA, amount: ctx.amount }; } return { kind: DONE, result: REJECTED }; } return { kind: DONE, result: REJECTED }; case MANAGER_REVIEW: return { kind: DONE, result: APPROVED }; case FINANCE_REVIEW: return { kind: DONE, result: APPROVED }; case AUTO_APPROVED: case DONE: return status; } }这里返回的永远是OrderStatus类型里定义的节点不允许返回一个不存在的状态。6.4 第四步驱动整个流程有了transit函数就可以循环直到流程结束function runWorkflow(start: OrderStatus, ctx: OrderContext): OrderStatus { let current start; let guard 0; while (current.kind ! DONE guard 10) { current transit(current, ctx); guard; } return current; }guard防止死循环这在演示代码里是必要的保护。生产环境建议用有向图或者最大步数限制后面最佳实践会展开讲。到此为止一个最简的“有类型条件工作流”已经完整了。它没有引入任何框架只靠 TypeScript 的类型系统就完成了流程节点类型化条件类型化分支穷尽性编译期检查流转逻辑集中收敛。7. 完整示例与代码实现上面的拆解是分步骤的这一节直接给出一个可以复制运行的完整示例。先给 TypeScript 版本再给 Java 版本。7.1 TypeScript 完整示例创建一个src/workflow.ts// 文件路径src/workflow.ts export type OrderStatus | { kind: CREATED } | { kind: AUTO_APPROVED } | { kind: MANAGER_REVIEW; manager: string } | { kind: FINANCE_REVIEW; manager: string; amount: number } | { kind: DONE; result: APPROVED | REJECTED }; export type Condition | { kind: amountGt; value: number } | { kind: amountLt; value: number } | { kind: hasRole; role: string } | { kind: and; left: Condition; right: Condition } | { kind: or; left: Condition; right: Condition }; export interface OrderContext { orderId: string; amount: number; userRoles: string[]; } export function evaluateCondition(cond: Condition, ctx: OrderContext): boolean { switch (cond.kind) { case amountGt: return ctx.amount cond.value; case amountLt: return ctx.amount cond.value; case hasRole: return ctx.userRoles.includes(cond.role); case and: return evaluateCondition(cond.left, ctx) evaluateCondition(cond.right, ctx); case or: return evaluateCondition(cond.left, ctx) || evaluateCondition(cond.right, ctx); default: { const _exhaustive: never cond; throw new Error(Unhandled condition kind: ${JSON.stringify(_exhaustive)}); } } } export function decideNextStatus(status: OrderStatus, ctx: OrderContext): OrderStatus { switch (status.kind) { case CREATED: { if (ctx.amount 1000) { return { kind: AUTO_APPROVED }; } if (ctx.amount 10000) { if (ctx.userRoles.includes(MANAGER)) { return { kind: MANAGER_REVIEW, manager: managerA }; } return { kind: DONE, result: REJECTED }; } if (ctx.userRoles.includes(MANAGER) ctx.userRoles.includes(FINANCE)) { return { kind: FINANCE_REVIEW, manager: managerA, amount: ctx.amount }; } return { kind: DONE, result: REJECTED }; } case MANAGER_REVIEW: return { kind: DONE, result: APPROVED }; case FINANCE_REVIEW: return { kind: DONE, result: APPROVED }; case AUTO_APPROVED: case DONE: return status; } } export function runWorkflow(start: OrderStatus, ctx: OrderContext): OrderStatus { let current start; let guard 0; while (current.kind ! DONE guard 10) { console.log(step${guard 1}, current${current.kind}); current decideNextStatus(current, ctx); guard; } return current; }创建src/main.ts// 文件路径src/main.ts import { OrderStatus, OrderContext, runWorkflow } from ./workflow; const cases: Array{ name: string; ctx: OrderContext } [ { name: 小额订单自动通过, ctx: { orderId: A001, amount: 500, userRoles: [USER] }, }, { name: 普通金额订单有经理审批权, ctx: { orderId: A002, amount: 5000, userRoles: [USER, MANAGER] }, }, { name: 大额订单缺少财务审批权拒绝, ctx: { orderId: A003, amount: 50000, userRoles: [USER, MANAGER] }, }, { name: 大额订单有经理和财务审批权进入财务审批, ctx: { orderId: A004, amount: 50000, userRoles: [USER, MANAGER, FINANCE] }, }, ]; const start: OrderStatus { kind: CREATED }; for (const c of cases) { console.log(\n ${c.name} ); const result runWorkflow(start, c.ctx); console.log(final status:, JSON.stringify(result)); }运行npx ts-node src/main.ts预期输出 小额订单自动通过 step1, currentCREATED final status: {kind:AUTO_APPROVED} 普通金额订单有经理审批权 step1, currentCREATED final status: {kind:MANAGER_REVIEW,manager:managerA} 大额订单缺少财务审批权拒绝 step1, currentCREATED final status: {kind:DONE,result:REJECTED} 大额订单有经理和财务审批权进入财务审批 step1, currentCREATED final status: {kind:FINANCE_REVIEW,manager:managerA,amount:50000}你会发现这个演示没有处理MANAGER_REVIEW和FINANCE_REVIEW到DONE的完整过程。这是因为runWorkflow默认从CREATED开始连续流转如果您需要模拟审批人点击“通过”按钮只需要调用decideNextStatus并传入当前状态即可const status1: OrderStatus { kind: MANAGER_REVIEW, manager: managerA }; const ctx: OrderContext { orderId: B001, amount: 5000, userRoles: [USER, MANAGER] }; const next decideNextStatus(status1, ctx); console.log(JSON.stringify(next)); // {kind:DONE,result:APPROVED}7.2 Java 完整示例基于 sealed interfaceJava 17 引入了密封接口sealed interface它非常适合表达“节点类型有限”的工作流模型。下面的代码不需要任何框架直接编译运行。创建一个Workflow.java// 文件路径Workflow.java import java.util.Set; public class Workflow { // 1. 定义上下文 public record OrderContext(String orderId, double amount, SetString userRoles) {} // 2. 定义节点类型sealed interface 限定实现类范围 public sealed interface OrderStatus permits Created, AutoApproved, ManagerReview, FinanceReview, Done {} public record Created() implements OrderStatus {} public record AutoApproved() implements OrderStatus {} public record ManagerReview(String manager) implements OrderStatus {} public record FinanceReview(String manager, double amount) implements OrderStatus {} public record Done(String result) implements OrderStatus {} // 3. 定义条件类型和评估器 public sealed interface Condition permits AmountGt, AmountLt, HasRole, And, Or {} public record AmountGt(double value) implements Condition {} public record AmountLt(double value) implements Condition {} public record HasRole(String role) implements Condition {} public record And(Condition left, Condition right) implements Condition {} public record Or(Condition left, Condition right) implements Condition {} public static boolean evaluate(Condition cond, OrderContext ctx) { return switch (cond) { case AmountGt c - ctx.amount() c.value(); case AmountLt c - ctx.amount() c.value(); case HasRole c - ctx.userRoles().contains(c.role()); case And c - evaluate(c.left(), ctx) evaluate(c.right(), ctx); case Or c - evaluate(c.left(), ctx) || evaluate(c.right(), ctx); }; } // 4. 流转函数 public static OrderStatus transit(OrderStatus status, OrderContext ctx) { return switch (status) { case Created ignored - decideFromCreated(ctx); case ManagerReview ignored - new Done(APPROVED); case FinanceReview ignored - new Done(APPROVED); case AutoApproved ignored - status; case Done ignored - status; }; } private static OrderStatus decideFromCreated(OrderContext ctx) { if (ctx.amount() 1000) { return new AutoApproved(); } if (ctx.amount() 10000) { if (ctx.userRoles().contains(MANAGER)) { return new ManagerReview(managerA); } return new Done(REJECTED); } if (ctx.userRoles().contains(MANAGER) ctx.userRoles().contains(FINANCE)) { return new FinanceReview(managerA, ctx.amount()); } return new Done(REJECTED); } // 5. 驱动流程 public static OrderStatus run(OrderStatus start, OrderContext ctx) { OrderStatus current start; int guard 0; while (!(current instanceof Done) guard 10) { System.out.println(step (guard 1) , current current); current transit(current, ctx); guard; } return current; } public static void main(String[] args) { var ctx new OrderContext(A004, 50000, Set.of(USER, MANAGER, FINANCE)); var start new Created(); OrderStatus finalStatus run(start, ctx); System.out.println(final status finalStatus); } }编译运行javac Workflow.java java Workflow预期输出step1, currentCreated[] final statusFinanceReview[managermanagerA, amount50000.0]这个示例里sealed interface OrderStatus明确了订单流程只有五种节点编译器会强制switch分支覆盖所有类型。如果后续新增一种Canceled节点transit方法必须同步更新否则编译失败。这就是“有类型写法”在 Java 里的体现。8. 有类型写法与传统工作流引擎方案的对比很多团队在遇到工作流需求时第一反应是引入 Flowable、Camunda 这类重型引擎再配一套 BPMN 图形界面。这套方案有它的适用场景但和“有类型写法”解决的问题并不完全重合。维度有类型写法传统 BPMN 引擎学习成本低只需要掌握语言类型系统高需要学习 BPMN 规范、部署模型启动成本零依赖直接写代码需要部署引擎、建表、配数据源条件建模用类型和代码描述编译期检查用表达式字符串如 SpringEL、JUEL运行时解析流程可视化无需要靠代码阅读或额外开发自带图形化设计器动态变更流程不擅长修改需要发版擅长可通过模型部署动态更新适合场景流程相对固定、规则复杂但可枚举流程经常调整、需要业务人员配置一个重要判断如果你们的业务流程半年才变一次规则又比较清晰有类型写法是更划算的选择。只有当你面临“业务人员需要频繁调整流程节点”时BPMN 引擎的图形化配置能力才值得那套运维成本。9. 运行结果与效果验证上面两个示例都能直接运行但运行成功不代表代码模型是对的。这里给出更完整的验证思路。9.1 验证编译期的穷尽性这是有类型写法最核心的价值。我们可以做一个测试把 TypeScript 里OrderStatus的AUTO_APPROVED分支注释掉。case AUTO_APPROVED: case DONE: return status;改成case DONE: return status;TypeScript 编译器会报错提示AUTO_APPROVED没有被处理。Java 同理如果sealed interface的switch漏掉某个实现类编译也会失败。这个验证每一名读者都可以自己动手做能够直观感受到“编译器帮忙兜底”的效果。9.2 验证业务逻辑业务逻辑的验证还是要靠测试。给decideNextStatus写单元测试是最直接的方式。这里给出 TypeScript 版的极简测试思路// 文件路径src/workflow.test.ts import { decideNextStatus, OrderStatus, OrderContext } from ./workflow; function assertEqual(actual: OrderStatus, expected: OrderStatus) { const a JSON.stringify(actual); const e JSON.stringify(expected); if (a ! e) { console.error(FAIL: expected ${e}, got ${a}); process.exit(1); } console.log(PASS: ${a}); } const ctx: OrderContext { orderId: T001, amount: 5000, userRoles: [USER, MANAGER] }; const start: OrderStatus { kind: CREATED }; const next decideNextStatus(start, ctx); assertEqual(next, { kind: MANAGER_REVIEW, manager: managerA });运行npx ts-node src/workflow.test.ts在真实项目中您可以用 Jest 或 Vitest 替代这套手写断言。重点不是测试框架而是条件判断本身已经被集中到一个函数里测试成本很低。9.3 验证失败时先看哪里如果出现异常按这个顺序排查编译期是否报错如果是说明新增的节点或条件类型没有在switch里处理这是类型系统在提醒你补分支。运行结果不符合预期先打印当前状态和上下文确认ctx数据是否正确。流程卡在循环里检查guard计数是否足够生产环境建议用有向图判断无环。10. 常见问题与排查思路问题现象可能原因排查方式解决方案TypeScript 编译报“not all code paths return a value”switch某个 case 没有返回值或条件分支不完整检查transit和evaluateCondition的所有 case用穷尽性检查never类型或补全所有分支Java 编译报“switch expression does not cover all possible input values”sealed interface新增实现类但switch没同步更新查看报错位置对照密封接口的实现类列表在switch中补上新增类型的分支流程走到了错误的节点业务条件判断本身写错打印ctx和每一步的current状态为decideNextStatus补充测试用例覆盖边界金额多个业务方各自实现一套条件逻辑代码重复没有统一建抽象层全局搜索条件判断的散落位置按本文方法把条件收敛为统一类型和解释器想给条件增加新类型需要同步修改的类型较多从Condition类型定义开始逐层看switch先改类型定义再依赖编译器列出所有受影响位置流程出现死循环流转函数里存在环例如 A - B - A检查每一步的跳转逻辑查看是否遗漏终止条件增加步数上限或提前做有向图无环校验11. 有类型写法的局限性必须说清楚有类型写法不是银弹。它的适用边界要明确。11.1 不擅长动态流程变更如果你需要业务人员在管理后台拖拽流程节点、实时发布新流程有类型写法会很别扭。因为节点和条件都是编译进代码里的动态变更就需要引入脚本引擎或规则引擎复杂度反而上去了。11.2 调试 JSON 序列化后的条件时体验一般把Condition序列化成 JSON 存储没问题但反序列化回来时需要自己处理kind字段和实际类型的映射。这比直接调用代码多了一层转换出错时要多排查一步。11.3 跨语言协作时需要额外约定如果部分服务用 Java部分用 GoCondition这个类型在不同语言里各写一遍就失去了“单一来源”的优势。这种情况需要考虑是否引入 schema 定义如 JSON Schema来约束。11.4 类型系统不是业务流程文档类型能约束数据结构但不能替代流程图。团队里如果同时需要给非技术人员讲解流程建议额外维护一份简单的状态图文档代码负责“做对”文档负责“看懂”。12. 最佳实践与工程建议写到这里把实际落地时会用到的经验整理成清单。12.1 从最小模型起步不要一开始就建通用引擎“条件工作流”范围很广。有的人需要串行条件分支有的人需要并行汇聚有的人需要子流程。建议第一个版本只支持最简单的顺序流转和条件分支不要加入并行、回退、超时提醒这些高级特性。等跑通一两个真实流程后再根据瓶颈决定是否扩展。过早抽象是这类代码最常犯的错误。12.2 用穷尽性检查锁住分支完整性TypeScript 用neverJava 用sealed interface加完整switch。这两个技巧是“有类型写法”中最值得推广的实践。成本几乎为零收益却很大——每次新增节点都会迫使你思考所有流转路径。12.3 条件判断收敛到单一解释器不要让业务代码里到处写if (amount threshold)这样的判断。把所有条件抽象成Condition然后只在evaluateCondition这一个地方处理。这样条件规则变更时只需要改类型定义和解释器测试也只需要覆盖这一个函数。12.4 上下文对象保持不可变流转函数要尽量避免直接修改传入的上下文。每次调用transit返回新状态而不是在原来对象上改字段。这不仅能避免并发问题也让每一步流转变得可追溯、可回放。12.5 给流程加上最大步数保护和环检测生产环境的工作流必须有防死循环机制。最简单的做法是限制最大流转步数如果流程更复杂可以在启动前对状态图做一次 DFS检测是否存在从起点可以到达的环。12.6 事件日志记录每一步流转无论什么类型的工作流都要记录完整的流转历史。建议至少记录当前状态、目标状态、发生时间、触发人/触发系统、上下文关键字段快照。这些日志既是排查问题的依据也是后续做流程分析的基础数据。12.7 条件可以序列化到配置中心但入口要收敛如果业务上确实需要动态调整阈值可以把Condition序列化成 JSON 放到配置中心。但注意反序列化和校验逻辑必须收敛在同一个模块里不要散落在各个服务中各写各的。12.8 版本兼容策略流程代码一旦发布历史数据可能还停留在旧的状态节点。新增节点类型时要考虑老数据的兼容旧状态如何映射到新类型是否需要做数据迁移建议状态类型中的kind字段不要轻易改名新增节点时尽量走“新增分支”而不是“修改旧分支”的路径。13. 总结与后续学习方向这篇文章围绕“条件工作流的有类型写法”做了三件事第一讲清楚了无类型写法的问题——字符串散落、编译期不可检查、分支不可穷尽。这些问题在小型项目里不明显但随着流程增多会迅速放大。第二用 TypeScript 和 Java 给出了完整的可运行示例。核心思想是把“条件”建模成类型把“节点”建模为判别联合或密封接口然后用解释器统一执行。两个示例都没有引入任何框架可以直接落进现有工程。第三给了落地建议和局限性说明。有类型写法不是要替代 Flowable 这类 BPMN 引擎而是给“流程相对固定、规则可枚举”的业务提供一种更轻、更安全的建模方式。如果你准备在自己的项目里实践建议按这个顺序推进先把你现有代码里所有if (xxx.equals(yyy))这样的条件整理出来看能不能抽成一个枚举或联合类型挑一个最典型的工作流节点用本文的Condition结构重新建模跑通后用穷尽性检查体验一次“编译器帮你找出漏掉的分支”再决定是否要把所有流程都迁移到这种写法。后续可以继续深入的方向包括工作流持久化方案状态与事件怎么存储、条件表达式与脚本语言的取舍、并行分支与汇聚节点建模、以及工作流监控与可视化。每一个方向都可以基于本文的模型继续扩展。
返回列表