从if-else地狱到规则流引擎:ruflo轻量级实践与核心源码解析
2026/9/9 9:11:54 网站建设 项目流程

ruflo 这个名字,是我给自己做的那个轻量级规则流引擎起的。取 rule 和 flow 两个词各截一段拼在一起,意思很直白:把规则排成一条流,让数据顺着流走完,就知道该做什么了。做这个东西的起因很实际,当时我负责的促销订单逻辑已经难改到离谱——满减、折扣、赠品、会员等级、优惠叠加全部塞在一个 Service 方法里,里面累计嵌套了十几层 if-else,一条规则变了就要发版,一个分支错了线上立刻出问题。我最初只是想把这些判断从代码里剥出去,没想到最后做了一个小引擎出来。

这篇文章会把 ruflo 从想法、建模到核心代码实现完整讲一遍,包括我在真实业务里踩过的坑、被坑过的并发问题,以及最后它长成了什么样子。适合那些正在被复杂业务判断折磨、想自己动手写一套轻量规则编排工具的开发者,也适合刚接触规则引擎、想理解底层原理的新手。

1. ruflo 从哪来的:一张被 if-else 塞满的促销订单

1.1 那个让我想重构的 Monday Morning

事情是周一早上开始的。运营在群里说,凌晨那波限时折扣和会员叠加算错了,用户实际支付的金额比预期多了十几块钱,工单已经打到技术侧。我打开那个出名的PromotionService,顺着条件一层层往下翻:先是判断是否新手、再判断品类、再判断会员等级、再判断是否在活动时间窗、再判断优惠券是否可用……一个方法小两百行,光if就有四五十个。最后定位到的问题其实特别小:新人专享价和限时秒杀之间的优先级写反了,运营在后台加了一个新活动类型,代码里的顺序没跟上。

这种问题技术难度为零,却花了我快两个小时。真正让我难受的不是那一次线上问题,而是我发现这个类已经变成了一团谁都说不清楚“到底什么规则在生效”的毛线球。运营改了配置,代码没有对应分支;代码加了分支,配置又没同步;两边各改各的,最终只能靠线上故障来暴露。

需求本身从来不复杂:无非是“满足某些条件 → 执行某个动作”。复杂的是条件越来越多、动作越来越丰富、排列组合越来越无规律可循。当这种判断逻辑全部长在代码里,代码就沦为业务规则的活文档,而且是没人维护的那种。

1.2 为什么不用现成的规则引擎

想解决这个问题,第一反应是引入现成规则引擎。我确实认真调研过 Drools、Easy Rules、Flowable 这些方案,结论是它们都很强,但和当时的场景不太匹配。

Drools 的规则语法 DRL 非常强大,Rete 算法的匹配效率也很高,适合规则多、模式匹配复杂的场景。但它的问题是学习曲线太陡,团队里大多数人没写过 DRL,光一个“规则优先级和冲突解决策略”就能把人绕晕。而且 Drools 引入进来之后,规则文件的管理、调试、单测也要跟着搭一套,对一个小团队来说是大炮打蚊子。

Flowable 和 Activiti 这类工作流引擎又是另一个方向,它们解决的是“人参与的任务流转”,注重环节之间的审批、会签、驳回,本质是状态机加任务分配。而促销规则这种场景,更多是“数据进来 → 判断 → 计算 → 输出”,不需要人来审批,用工作流引擎反而显得笨重。

至于 Groovy 脚本热加载这种野路子,灵活是灵活,但没有结构,脚本多了以后依然是每个脚本一个世界,彼此之间怎么衔接、怎么组合、怎么可视化,全靠自觉。

我真正想要的,是一个刚好的东西:能描述一条线性的判断流程,每个环节是可复用的小单元,条件用配置表达而不是写死在代码里,同时小到任何一个开发都能在半小时内看懂它的所有代码。

1.3 ruflo 的定位:用一个文件描述一条规则流

所以 ruflo 的定位从一开始就没有摇摆:不做一个通用的规则语言,做一个能描述“流程式规则判断”的轻量引擎。它把业务判断拆成节点,节点之间用连线组成流,数据带着上下文从流的起点走到终点,每经过一个节点就完成一次判断或动作。

比如一个典型的促销场景,用 ruflo 表达大概是这样的:

{ "flowId": "promotion-order", "nodes": [ { "id": "start", "type": "start", "next": ["check-user"] }, { "id": "check-user", "type": "condition", "expression": "user.level == 'vip' || user.isNewUser", "trueNext": ["apply-vip-discount"], "falseNext": ["apply-normal-price"] }, { "id": "apply-vip-discount", "type": "action", "action": "discountByLevel", "next": ["end"] }, { "id": "apply-normal-price", "type": "action", "action": "normalPrice", "next": ["end"] }, { "id": "end", "type": "end" } ] }

配置只描述“去哪些节点、按什么条件跳转”,节点真正执行的逻辑仍然在代码里,但每个节点是一个独立的小方法,可以被复用、被单测、被替换。运营改规则时,只需要调整表达式的条件和跳转关系,不需要动代码。这基本就是 ruflo 最初的样子,后来在真实业务里长了不少肌肉,但骨架一直没变。

2. ruflo 的核心模型:节点、上下文与规则流

2.1 Node:最小的可执行单元

我设计 ruflo 时反复提醒自己一件事:越小的引擎,核心概念越要克制。所以整个模型只有三个概念:Node(节点)、Context(上下文)、Flow(规则流)。

Node 是最小执行单元,它有唯一的 id、一个执行入口、以及若干个“下一跳”指向。它的 Java 接口我写得非常简单:

public interface Node { String getId(); String execute(FlowContext ctx); }

execute返回的是一个字符串,代表下一个要执行的节点 id。如果返回null或空串,则表示这条流走到尽头。这里没有trueNext/falseNext这种硬编码概念,而是让节点自己决定接下来去哪。这样设计的好处是节点类型不会被写死,条件节点可以返回多种路径,动作节点也可以动态决定后面的走向,灵活性集中在节点内部,而不是被框架约束。

我把实际用到的节点类型归纳成这么几类:

节点类型作用典型场景
start / end标记流程起点和终点所有流
condition解析表达式,按结果选择路径用户是否符合某个条件
action执行一段业务动作,修改上下文计算折扣、下发优惠券
switch多分支,类似多路条件订单类型分桶
loop循环执行子流遍历商品行计算总价
script执行一段受限制的表达式脚本临时赋值、日志、数据清洗

每种节点都实现 Node 接口,框架只认接口,不关心节点内部干了什么。这就是组合模式的好处,后续想扩展新节点类型,只需要加一个类,注册一下,流程配置里就能直接引用。

2.2 Context:数据在流里怎么传

节点之间靠 Context 传递数据。一开始我图省事,直接用了一个Map<String, Object>,后来发现裸 Map 有几个问题:第一,业务对象和临时变量全混在一起,命名很容易撞车;第二,节点之间能看到彼此所有的变量,有些中间结果脏数据会被后面误读;第三,并行执行的时候多个节点同时写同一个 key,数据竞争防不胜防。

所以后来我把 Context 包装成了一个带作用域的结构。它看起来还是 Map 的用法,但内部划分了三个区域:全局变量、流内临时变量、只读快照。全局变量适合放订单、用户这类从头到尾都要用的业务对象;临时变量适合放某个中间计算结果,离开当前作用域就能被回收;只读快照用于挂载外部查询出来的、不允许被节点修改的源数据。

public class FlowContext { private final Map<String, Object> globals = new ConcurrentHashMap<>(); private final Map<String, Object> locals = new ConcurrentHashMap<>(); private final Map<String, Object> snapshot = new ConcurrentHashMap<>(); public Object get(String key) { if (locals.containsKey(key)) return locals.get(key); if (globals.containsKey(key)) return globals.get(key); return snapshot.get(key); } public void setGlobal(String key, Object value) { globals.put(key, value); } public void setLocal(String key, Object value) { locals.put(key, value); } public void setSnapshot(String key, Object value) { snapshot.putIfAbsent(key, value); } }

一个容易被忽略的设计是:动作节点执行完以后产生的中间数据,尽量写进 locals,而不是 globals。比如某个规则算出了优惠金额,如果直接塞进全局,下一个节点很容易不小心读到它,造成“幽灵数据”。locals 随着流程阶段清理,能有效减少这类问题。

2.3 FlowDefinition:用 JSON 替代代码描述判断

有了 Node 接口和 Context,还需要一个能描述“流长什么样”的配置结构。我选择了 JSON,没有发明自己的 DSL。FlowDefinition 主要包含三部分:节点列表、每个节点的连线关系、以及每个节点需要的参数。

节点列表就是nodes数组,里面是 id、type、参数、连线。连线我埋在一个通用字段里,条件节点用它做分支,普通节点只用一个 next。参数的表达方式也很统一,全部是字符串值,节点执行时再按需解析。

{ "flowId": "order-calculate", "version": "20250116", "nodes": [ { "id": "gate", "type": "condition", "params": { "expression": "order.totalAmount >= 500" }, "trueNext": "full-reduction", "falseNext": "normal-price" }, { "id": "full-reduction", "type": "action", "params": { "action": "calcFullReduction", "threshold": "500", "reduce": "80" }, "next": "end" }, { "id": "normal-price", "type": "action", "params": { "action": "calcNormalPrice" }, "next": "end" }, { "id": "end", "type": "end" } ] }

选择 JSON 而不是自研 DSL,是因为这个项目从一开始就是给团队用的,不是给个人自嗨的。JSON 的好处人人都会写,可以被 Git 管理,可以被后台系统编辑,可以被程序校验。DSL 虽然表达力更强,手感更优雅,但团队里每个人都要重新学语法,调试还要单独写解析器,成本一下就上去了。

2.4 关于“为什么选 JSON 而不是 DSL”的取舍

如果这是一篇纯粹的理想主义技术分享,我可能会说 DSL 更好。但在真实项目里,我需要考虑的是团队协作成本。

DSL 的优雅是给写 DSL 的人看的,读的人未必买账。一个运营配置人员可能只改过一个参数,为了完成这个改动,他需要理解 DSL 的 token、嵌套规则、转义字符——这些学习成本足够劝退。而 JSON 是绝大多数技术人都见过的格式,缩进清晰、结构直观、出错时有明确的提示。

另一个原因是 JSON 可以无缝对接可视化配置界面。我后续给规则流做的简易后台,直接把 JSON 的 key 映射成表单控件,条件、分支、参数都从 JSON 里读出来,用户在下拉框里选一选就能改配置。如果是自研 DSL,还得再写一层编译器和表单之间的转换桥。这一点在规划阶段就让我彻底放弃了自研 DSL 的念头。

当然 JSON 也有缺点:表达复杂嵌套条件时不够优雅,注释用不了,字段多了之后有大量重复。我的应对是:不在 JSON 里塞超长表达式,超过三层的条件一律拆成多个 condition 节点串联。这样配置变得更啰嗦,但每个节点足够原子化,肉眼就能看清楚每一条判断规则,整体上还是划算的。

3. 从零实现一个最小可用的 ruflo 内核

3.1 四个核心类/接口

ruflo 的内核代码很少,核心就四个东西:Node 接口、FlowContext、FlowDefinition 和 FlowEngine。前面两个已经介绍过,FlowEngine 是整个执行器的核心,负责加载定义、构建节点映射、按顺序执行节点。

public class FlowEngine { private final Map<String, Node> registry = new HashMap<>(); public void register(Node node) { registry.put(node.getId(), node); } public void execute(FlowDefinition def, FlowContext ctx) { Node currentNode = registry.get(def.getStartNodeId()); int guard = 0; int maxSteps = def.getMaxSteps() > 0 ? def.getMaxSteps() : 1000; while (currentNode != null) { if (++guard > maxSteps) { throw new IllegalStateException("flow step exceeds limit, maybe endless loop: " + def.getFlowId()); } String nextId = currentNode.execute(ctx); currentNode = (nextId == null || nextId.isEmpty()) ? null : registry.get(nextId); if (currentNode == null && nextId != null && !nextId.isEmpty()) { throw new IllegalStateException("target node not found: " + nextId); } } } }

registry是节点 id 到 Node 实例的映射,execute是主循环。我没用递归,而是 while 循环,这是故意为之。递归写法看起来简洁,但深链路或者异常跳转会带来栈溢出和理解成本,while 循环配合同样结构,怎么走都不会爆栈。

节点实例用register手动注入,而不是靠反射扫描。手动注入的好处是团队里任何人看FlowEngine的组装代码,就能知道哪些节点可用,不会有“这个节点到底注册了没有”的猜测。

3.2 条件表达式怎么安全求值

条件节点是规则流里最核心的节点,它本质上要回答一个问题:这个表达式在当前上下文里成立吗?

表达式求值有两个选择:引入现成表达式引擎,或者自己写。我选了前者,因为 SpEL 已经足够成熟,而且它能限制反射调用。关键点在于,不能用默认的StandardEvaluationContext,要用SimpleEvaluationContext,后者不带反射、不带类型构造器,只能调用注册过的白名单方法,安全边界清晰很多。

public class ConditionNode implements Node { private final ExpressionParser parser = new SpelExpressionParser(); private final String trueNext; private final String falseNext; public ConditionNode(String id, String expression, String trueNext, String falseNext) { this.id = id; this.expression = expression; this.trueNext = trueNext; this.falseNext = falseNext; } @Override public String execute(FlowContext ctx) { EvaluationContext evalCtx = new SimpleEvaluationContext.Builder() .withInstanceMethods() .build(); evalCtx.setVariable("ctx", ctx); evalCtx.setVariable("order", ctx.get("order")); evalCtx.setVariable("user", ctx.get("user")); Boolean result = parser.parseExpression(expression).getValue(evalCtx, Boolean.class); return Boolean.TRUE.equals(result) ? trueNext : falseNext; } }

注意parseExpression这行,我把解析结果缓存起来了。SpEL 表达式每次 parse 的成本不低,尤其在高频调用场景下,反复 parse 会白白浪费 CPU。同一份 FlowDefinition 第一次加载时把表达式编译好,后面执行直接复用,快得不明显但不积小流无以成江海。

至于表达式里能访问什么对象,我在创建 EvaluationContext 时显式塞进变量。这比让表达式自己从某个全局容器里捞变量要安全得多,表达式只能看见我允许它看见的东西。

3.3 执行引擎:顺序、分支与合并

大多数规则流并不需要复杂图结构,顺序加分支就能覆盖八成场景。ruflo 的主循环天然支持顺序和分支,因为节点自己返回下一个目标 id,顺序就是“每个节点都返回配置里的 next”,分支就是 condition 节点根据表达式返回 trueNext 或 falseNext。

合并的处理稍微微妙一点。两个分支最终要汇合到同一个节点,这在图结构上是典型的多入边节点。在 ruflo 里不需要特殊处理,因为每个分支跑完都会返回到同一个聚合节点 id。比如满减分支走完full-reduction,普通分支走完normal-price,两个节点都把 next 指向end,引擎把控制权交给end,自然就合并了。

// 分支合并不需要额外代码,节点把 next 指向同一个节点即可 "full-reduction": { ..., "next": "end" } "normal-price": { ..., "next": "end" }

如果业务需要等待多个并行分支全部完成再继续,那就复杂多了,得引入 parallel gateway/ForkJoin 之类的结构。我在实际业务里遇到过一次,当时直接在节点内部用CompletableFuture并行执行了子流程,等所有分支都完成后再返回统一的下一个节点。这是一种偷懒但有效的做法,因为并行节点之间共享 Context 的问题已经在框架层解决了,业务侧只需要关心任务怎么拆分。

3.4 让循环可控:while 节点的防死循环设计

规则流一旦支持循环,就引入了死循环的可能性。很多规则引擎在设计上干脆不提供循环节点,逼着用户用递归子流来绕。ruflo 我保留了循环,因为真实业务里有遍历商品行、遍历优惠券集合这种需求,没有循环的规则流写起来太痛苦了。但循环必须受控。

我给 loop 节点设了三个硬性约束:最大循环次数、循环计数器、以及上下文中的迭代变量。最大循环次数在 FlowDefinition 里配置,默认 1000,配置得再大也会被引擎全局的maxSteps拦住。

public class LoopNode implements Node { private final String loopVar; private final String collectionExpr; private final int maxLoop; private final String bodyNodeId; @Override public String execute(FlowContext ctx) { Collection<?> items = evaluate(collectionExpr, ctx); if (items.size() > maxLoop) { throw new IllegalStateException("loop size exceeds maxLoop: " + items.size()); } ctx.setLocal(loopVar, items.iterator()); ctx.setLocal(loopIdxVar, 0); return bodyNodeId; } }

body 节点执行完毕后,会返回一个特殊标记__loop_continue__,引擎识别到这个标记就回到 loop 节点继续下一次迭代,直到迭代器耗尽。这个设计不算优雅,但它简单可解释,任何一个开发读代码都能想清楚“什么时候进循环、什么时候出循环”。

跑几次测试之后我还发现,循环节点最容易出错的地方不是死循环,而是迭代变量没有清理。上一轮循环的变量残留在 Context 里,下一轮如果变量名相同就会读到脏值。所以 loop 节点结束前必须把loopVarloopIdxVar从 Context 里移除,这一点我在代码注释里写了三遍。

4. 调试与治理:规则流真正难的不是跑通,是让人看得懂

4.1 执行轨迹:把每一步都留下脚印

规则流跑通了只是第一步,真正麻烦的是线上出问题时怎么定位。在一个几十节点的长流里,数据经过哪些节点、哪个环节改变了最终结果,如果没有日志,排查起来会比以前 if-else 还痛苦。if-else 至少能加断点,配置化的规则流连断点都不知道该打在哪个“行号”上。

所以 ruflo 在引擎层内置了执行轨迹记录。FlowContext 里有一个 trace 列表,每走过一个节点就往里塞一条记录,包含节点 id、节点类型、执行耗时、节点读到的关键变量和写出的关键变量。正常状态下这层记录是关闭的,线上 CPU 和内存不受影响;只有当请求上下文里开启了 debug 标记时才记录。

有了这个执行轨迹,排查问题的方式从“猜”变成了“回放”。有一次线上用户反馈某个订单没有被算入满减,我把它的 flowId、trace 拉出来一看,发现它根本没走到满减节点,在 condition 节点就被拦住了。为什么被拦住?trace 里order.totalAmount显示是 499.99,而满减门槛是 500。差 1 分钱,这是一个典型的浮点精度问题而不是规则配置问题。如果没有 trace,这种问题至少要多花半小时。

4.2 配置漂移:没有版本管理的规则都是定时炸弹

规则流最大的隐患不是技术,而是治理。代码有 Git 有 Code Review,但配置化的规则太容易被随手改掉。今天运营在后台改一个阈值,明天开发直接改 JSON 配置,后天发现线上行为不一样了,但没人说得清是哪一次改动导致的。

我的应对方案是给 FlowDefinition 加版本号,并且在应用启动时把版本号打印到日志里。规则上线必须走配置发布流程,发布前有 diff 对比,发布后有生效时间,线上配置永远能对应到某个具体版本。

版本管理看起来是规则引擎之外的琐事,但它决定了一个规则引擎能不能被团队真正信任。没有版本管理的规则流,改多了就是一团乱麻,比 if-else 还难救。

我把 version 字段放在 JSON 的第一行,"version": "20250116",用日期做版本号,简单清晰。每次发布前只需要看 diff,就能知道这次改了哪些节点和表达式。

4.3 高性能与并发:从 ThreadLocal 到线程池的那一脚

并发问题是我在这个项目里踩得最深的坑。早期版本里,我为了图方便,把 FlowContext 放进了 ThreadLocal,想着这样节点执行时随时能拿到上下文,不用一处处传参数。看起来很美,运行起来就出事。

线程池里的线程是复用的,ThreadLocal 里的数据在线程执行完任务后如果没有手动清理,下一个任务就会读到上一个任务的上下文。规则 A 跑到一半,线程任务结束但没清理,规则 B 被同一个线程执行时,ThreadLocal 里还残留着规则 A 的订单数据。那一次事故直接导致用户收到了别人的优惠券,运营炸了,我也彻底意识到:ThreadLocal 是精细管理下的工具,不是偷懒的传送门

后来我把 ThreadLocal 全部删除,Context 改为方法参数显式传递。每个节点都能清晰地看到上下文从哪来、到哪去,线程池虽然还是那个线程池,但上下文跟着任务走,而不是跟着线程走,问题就此消失。

并行执行时的数据竞争是另一个坑。两个并行节点同时往 Context 里写同一个 key,结果取决于谁先落笔,这属于典型的非确定性行为。我在 Context 的set类方法里加了“同 key 重复写且值不同则抛异常”的保护逻辑,宁可让流程停下来,也不能让它脏着跑下去。规则计算没有幂等,脏数据一旦流到下游,造成的损失往往比一次运行失败大得多。

4.4 常见坑位与避雷对照表

把这些坑整理成一张表:

现象根因避雷方案
满减金额差一分钱浮点数直接比较金额用 Decimal 或者转分为整数比较
条件判断总是不命中表达式里字符串和数字类型不匹配在解析前做一次类型规范化
线上打了错误优惠Context 变量名撞车全局变量统一前缀,临时变量必须声明作用域
循环卡住 / CPU 飙升循环节点没有最大次数限制配置 maxLoop 加引擎级 maxSteps 双保险
线程池任务串数据ThreadLocal 跨任务残留禁用 ThreadLocal,Context 显式传递
配置有人改过但查不到流程配置没有版本管理每次发布留 diff,启动日志打印版本号

这表里的每一行,我都在真实环境里遇到过至少一次。规则引擎好不好用,不只看它能干多少事,还要看它能不能被人安全地维护。治理能力是规则的“负向能力”,越是在事故多发的时候,越能体现价值。

5. 在真实业务里长成什么样,以及下一步想做什么

5.1 营销规则中心的落地结构

ruflo 在业务里最终长成了一个“营销规则中心”。配置放在后台管理系统里,运营通过表单修改 JSON 配置,点击发布后配置进入版本管理表,应用侧通过配置中心实时拉取最新版本,FlowEngine 在本地解析并执行。

整个链路是这样的:

后台配置表单 -> FlowDefinition JSON -> 配置中心 -> 应用本地缓存 -> FlowEngine 执行

运营不再需要提工单等排期。满减门槛从 500 改成 300,打开后台改一个数,发布,十分钟后生效。而开发只需要维护节点对应的 action 方法,比如calcFullReduction接收金额、门槛、减免额,返回计算后的应付金额。规则怎么编排由运营侧通过配置控制,业务动作代码由开发控制,各司其职。

这个结构的核心红利是变更频次分离。业务规则的变动不再触发代码发布,风险面大幅缩小。以前改一条促销规则要全量回归整个订单计算链路,现在只需要回归涉及到的几个 action 节点,测试成本显著下降。

5.2 与人工审批流程的对比区别

做这个项目的过程中,我反复被问到一个问题:这跟 Flowable 有什么区别?我的标准回答是:Flowable 解决的是“人参与到流程中”的审批流转,而 ruflo 解决的是“数据在判断节点间流转”的规则计算,两者面向的实体完全不同。

审批流程里的元素是任务、是审批人、是会签与驳回,流程引擎需要管理任务的分配和超时回退。规则流里的元素是判断、是计算、是跳转,引擎需要考虑的是表达式的求值和节点的执行顺序。把促销折扣的规则放到工作流引擎里跑,或者把审批流转放到规则流引擎里跑,两边都会很难受。

这提醒我,做一个工具前先想清楚它到底要解决哪一类问题。不是所有叫“流程”的东西都可以共用同一个引擎,也不是越通用的框架越合适。“刚好够用”才是工程里最稀缺的品质。

5.3 后续扩展:可视化编排与回归测试

ruflo 现在最想补的一块能力是可视化编排。JSON 配置对于开发是直观的,对运营依然有门槛。我已经画好了原型的思路,把节点画成卡片,连线画成箭头,条件表达式展示在连线上,这样运营能看到一条“规则流”的全貌,而不是面对一堆 JSON 字段。

可视化之外,另一个更关键的基础设施是规则回归测试集。规则流最大的风险是“改了 A 规则影响 B 规则”,所以我把历史上有过线上问题的场景全部固化成黄金用例,每次有规则变更都自动跑一遍。黄金用例集看起来只是普通测试,但它实际承载的是规则引擎最核心的信任建设——让团队相信“改配置是安全的、可验证的”。

5.4 一些不太会写进文档的心里话

ruflo 并不是一个设计得多么精巧的引擎。它没有惊艳的算法,没有复杂的图论结构,很多地方甚至可以说是“土办法”。但恰恰是这种土,让团队里每个人都能读懂它、能维护它、能在出问题的时候按住它。

我现在越来越觉得,一个基础组件最核心的成功标准不是“功能强大”,而是“团队敢改、敢上线、敢排查”。ruflo 算不上一个完美的作品,但它做到了这三点。

如果让我重新做一次,我可能会把表达式安全边界做得更早、更彻底,把上下文作用域设计得更严格,但不会改变它的整体骨架。有些东西是在踩坑之后才真正理解的,比如:规则从代码里剥离出来不是目的,让规则的变更变得安全、可控、可追溯,才是目的。这一点,比引擎本身的实现方式重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询