news 2026/10/12 10:05:22

REA模型驱动的代码生成器:从交易建模到可维护的领域代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
REA模型驱动的代码生成器:从交易建模到可维护的领域代码

这个标题短得有点可疑,但 rea 确实是我最近两个月一直在打磨的一个内部小工具。名字来自 resource-event-agent 三个单词的首字母,它做的事情可以一句话讲完:把一份接近业务语言的交易模型描述,编译成一套可维护的领域代码骨架,经由它生成的代码可以直接落进现有的服务里,不需要再手工抄一遍字段。之所以强调“接近业务语言”,是因为我不想让团队在写配置时还得先学一套框架语法。

我把它做出来的直接原因是烦了。每次接手一个包含账户、订单、库存、对账模块的系统,我都要花大量时间做同一件事:把“谁在什么时间消耗了什么资源,产生了什么事件”翻译成表结构、状态机、DTO 和接口。这种翻译不困难,但极其消耗注意力,而且一旦业务规则在评审时变了一点,前端要重写组件,后端要改三个文件,数据库还要迁移,所有的重复劳动会再来一遍。

如果你也经历过这种“加班不是因为难,而是因为重复”的状态,那这篇文章应该对你有用。我会从模型本身的来龙去脉讲起,再带你走一遍 rea 的输入格式、生成逻辑和踩坑过程,最后聊聊哪些地方它完全不合适。项目不大,但解决的是一个非常普遍的建模问题。

1. 为什么一个代号 rea 的小工具能省掉我大半周的重复劳动

先说一个典型场景。某支付模块要新增“退款”能力。这个功能听起来很小,如果只写业务代码,可能两天就结束了。但真正落到系统里,你要动的东西远远不止一处:订单状态要新增一个refunded,账户流水表要加一个事件类型,对账文件要加一列,给前端的 DTO 要补字段,口语化的“退款”还必须对应到领域层的一个事件对象。

这些步骤之间的依赖关系非常固定。一个退款事件必然关联订单资源的“已支付”状态,也必然关联资金资源的“流入到用户余额”。也就是说,不管业务叫什么名字,它背后的资源流、事件流、参与者关系都是同一套骨架。真正的差别只在字段名、业务名和状态迁移规则。

我发现自己的大部分时间都花在了这套骨架上,而不是业务判断上。

1.1 重复的不是代码,而是“翻译”过程

我数了一下,某一次做类似功能时,我为了保持各层命名一致,光是“订单状态”这一个概念就在六个文件里出现了六次:数据库枚举、实体类、状态机定义、API 响应 DTO、兜底文案配置、测试数据构造器。手改两次还好,到了第三次,前面改了枚举、后面忘了改状态机,编译不报错,但运行起来直接抛非法状态异常。

这种错误不是靠细心能完全避免的。因为连接这些文件的不是代码之间的显式依赖,而是“人的记忆”。你记得去改 A,但 B 没有语法报错,只有线上出故障时你才想起来。

rea 的出发点就是把这层隐含的“翻译”变成显式的产物。想要新增一个事件类型,你只需要在模型文件里增加一条事件定义,重新跑一次生成,它会自动帮你把涉及这个事件的枚举、事件类、仓储接口和 API 占位全部同步掉,不会漏。

1.2 rea 到底是什么,它不做什么

先对齐一下边界,避免期望放得太高。rea 不是 ORM,不负责帮你写 SQL 的查询优化;rea 也不是完整的低代码平台,不会根据模型凭空长出一套可运行的后端服务。它更像一个“模型转代码”的编译器:输入是一份 YAML 格式的交易模型描述,输出是一组按固定约定生成的代码文件和数据库脚本。

你仍然要手写真正的业务方法,比如“这个退款事件需要校验用户余额是否足够”“这个充值事件需要调用外部支付网关回调”。rea 不帮你决定这些规则,它只保证所有文件在一开始就长得一模一样、叫得一模一样、引用得一模一样。

在我的工作流里,rea 的价值不是取代人,而是把人的精力从“保持一致”这个低价值动作中释放出来,留到“定义规则”这个真正值得思考的地方去。

2. 先把交易模型讲清楚:资源、事件、代理三者到底怎么配合

既然工具名字叫 resource-event-agent,那就得先把这三个概念讲透。我第一次看 REA 模型时觉得很简单:资源就是货和钱,事件就是付钱和交货,代理就是人和公司。但真到建模时,这三者的边界经常被我搞混。

2.1 一杯咖啡交易在 REA 模型里长什么样

拿最典型的“顾客花 10 元买一杯咖啡”举例。在普通的业务流程建模里,我们会画一张泳道图,把顾客、收银台、咖啡机都画上去。但在 REA 模型里,你只需要拆四样东西:

  • 资源(Resource):钱、咖啡。它们是被消耗或获取的对象。
  • 事件(Event):支付事件(钱从顾客口袋减少)、交货事件(咖啡从店员手里给到顾客)。
  • 代理(Agent):顾客、店员。它们是发起或参与事件的主体。
  • 约束:支付事件带来的“钱减少”必须对应交货事件的“咖啡增加”,两个事件通过一个交易闭环关联。

很多订单系统的建模思路其实是把这三层揉进了一张表里。订单既要当资源,又要当事件,还要记录操作人,结果导致订单表的字段膨胀得特别厉害,状态迁移也容易失控。分开建模之后,订单反而变成了一个“交易聚合”的标识,真正的行为发生在资源流和事件流上。

注意,这里的事件不是消息队列里的事件,而是“经济事件”,即资源发生增减的那一刻。这个东西在交易系统中对应的是流水、明细、快照,它比状态字段更原始,也更可靠。任何“当前余额等于 100 元”的说法,本质上都是“一系列资金流入事件减去一系列资金流出事件”的汇总结果。

2.2 从概念模型到代码模型的映射

概念拆分完之后,还要落进工程代码。我整理了一个在 rea 内部使用的映射关系,大部分团队可以直接照着用。

REA概念领域含义代码产物举例
Resource可被增减的存量概念账户实体、库存聚合、资产表
Event资源增减的事实记录流水记录、事件类、状态迁移记录
Agent参与交易的一方用户实体、商户实体、内部部门标识
Flow事件对资源的具体影响入账操作、出账操作、库存增减操作
Commitment对未来事件的事先承诺预订单、锁库存记录、预授权

这套映射是 rea 生成代码时最重要的约定。一个资源往往对应一个聚合根类,一个事件往往对应一个不可变的值对象,一个代理往往对应一个简单的角色接口。把这些约定写死在生成器里,只要模型描述不歧义,代码就不会出现“两个类表达同一个概念”的情况。

有一点很容易搞错:代理不是用户表的设计依据。用户系统中的手机号、昵称、头像仍然是通用用户属性,不该被 REA 模型管。REA 关注的是交易关系中“以什么身份出现”——同一个用户,可以是买方,也可以是卖方,所以代理在模型里应该是一个角色而不是一个人。

3. rea 的输入长什么样:一份贴近业务语言的交易描述

我见过不少代码生成工具,它们的输入是给技术团队准备的,要自己定义完整的 XML Schema 或依赖复杂的注解。rea 的输入则刻意做成了一份几乎可以直接拿给产品经理一起评审的 YAML 文件。下面是一个咖啡馆收银场景的模型描述。

model: coffee_shop version: "1.0" agents: - name: customer label: 顾客 - name: shop label: 店铺 resources: - name: money label: 资金 type: asset - name: coffee label: 咖啡 type: product flows: - name: pay label: 支付 resource: money direction: out source_event: sale_event - name: receive_coffee label: 收取咖啡 resource: coffee direction: in source_event: delivery_event events: - name: sale_event label: 销售事件 amount_field: amount participant_roles: [customer, shop] flows: [pay, receive_coffee] - name: delivery_event label: 交货事件 participant_roles: [shop, customer] flows: [receive_coffee]

如果熟悉领域驱动设计,你会觉得这本质上是一个“事件风暴”的精简输出。每个 agent 是泳道里的角色,每个 resource 是泳道里的实体,每个 event 是一张写着“发生了什么”的便利贴。

3.1 DSL 的最小字段集

设计这份 DSL 时,我刻意控制字段数量,因为每一多一个可选字段,生成器就多要一套分支逻辑,团队的认知负担也会跟着变大。目前最小集就五个节点:

  • agents:参与交易的角色列表,只关心角色身份。
  • resources:系统中流通的资产或物料,必须指明类型。
  • flows:一个事件对某个资源产生的影响,方向只有in和out两种。
  • events:资源增减的发生节点,必须挂至少一个 flow,也必须指定参与角色。
  • relations:显式描述代理和资源之间的归属关系,比如“咖啡属于店铺”。

我为什么没有把数量字段放在 flow 里?因为数量是事件发生时的结果,不是事件结构的定义。一个退款事件可能退全额也可能退部分,这个不确定性应该在生成出来的业务留白区域里处理,而不是写死在模型文件里。模型文件负责表达“有没有这层关系”,业务代码负责决定“这层关系具体怎么算”。

3.2 为什么字段这样设计

有个很现实的教训:第一次版本我的 DSL 里放了status_flow、audit_fields、notify_target这些字段,结果团队成员建模时每人填得都不一样,生成出来的代码五花八门,最后还是靠人肉对齐。后来我把 DSL 砍成上面这五个节点,新增一个业务模型变成三步:声明资源、声明角色、声明事件。

这样做的另一个好处是文件差异可读。产品经理说“我们不想区分‘支付’和‘收款’这两个事件了”,你只需要在 YAML 里删掉一个节点,git diff 一眼就能看明白影响范围,而不用在一堆代码文件里去对比哪个枚举被改了,哪个状态机漏改了。

4. rea 生成管线内部:解析、校验、渲染三步走

模型文件本身只是静态描述,真正麻烦的是怎么把它生成成有用且稳定的代码。rea 的生成管线借鉴了编译器的思路,但我只留下了三个阶段:解析、校验、渲染。每个阶段职责单一,后期加功能时不会互相牵绊。

4.1 解析:把模型文件变成内存对象

第一步是把 YAML 加载成结构化的 Python 数据类。我用了一个 YAML 解析库做这一步,但真正花功夫的不是加载,而是加载之后的类型转换。

比如resources[].type字符串,我要把它解析成枚举值;flows[].direction的in/out要转成布尔方向;events[].participant_roles要跟 agents 列表做一次引用绑定。解析层做完了,内存里就有一颗完整可遍历的模型树,后面渲染时只需要从根节点往下访问,不需要再回头翻 YAML。

这里有个小坑:YAML 解析出来的顺序在 Python 3.7 以后默认是保持插入序的,这个特性我依赖得很重。生成的代码文件顺序、数据库字段顺序、DTO 字段顺序,都来自模型文件里的声明顺序,而不是字典序或随机数,不然每次跑生成都会得到一堆 diff,代码评审就废了。

4.2 校验:拦截 80% 的手误

解析成功不代表模型合法。校验层是 rea 里我觉得最值得投入的部分。常见的校验规则很简单,但非常有效率:

  • 资源名不能与代理名重复,避免生成类时出现命名冲突。
  • events里引用的flows必须真实存在,且每个 flow 都必须挂在至少一个 event 上。
  • 两个 event 不能同时拥有同名同方向的 flow,否则语义上会出现资源归属不明。
  • 角色名必须在agents中定义过,不能凭空造一个参与者。

这些规则看起来是常识,但我实测下来能拦截掉团队协作时大部分低级错误。有一次一个同事在模型文件里把customer拼错成customr,校验层直接报错,生成的代码连影子都没见到。如果没有这层校验,这个问题大概率会变成一个运行时异常,而且要在消费者页面上才能暴露。

更让我意外的是,校验规则还能反过来帮建模者理清思路。有人建模时把“库存”既当成 resource 又当成 agent 来用,校验器提示“资源与代理重名”之后,他自己就发现是概念混淆了。

4.3 渲染:从模型到代码

渲染层是所有工程约定的集中地。rea 目前支持生成四类产物:

  • 领域对象:资源实体、事件值对象、代理角色接口。
  • 数据访问接口:按资源聚合根生成仓储方法签名。
  • API 骨架:为每个事件生成一个入参 DTO 和一个响应占位。
  • 数据库脚本:生成基础表和事件明细表的建表 SQL。

渲染的时候我用模板文件,每个模板里只有变量替换和循环枚举,没有复杂逻辑。比如实体模板的一部分长这样:

from dataclasses import dataclass from datetime import datetime @dataclass class {{ entity_name }}: id: str {{ modifier_fields }} created_at: datetime updated_at: datetime

你可以看到 rea 不生成任何查询逻辑,生成的全是“形状稳定”的代码。真正的方法体用一个# TODO: implement business rule占位,开发需要在生成后补齐。我会在下一个章节讲为什么这个“留白”反而让工具更好用。

5. 跑通第一个示例后,我补上的三个工程化细节

rea 第一次能跑通时,我觉得大功告成。但放到真实项目里用了三天,我就意识到“能生成代码”和“能安全地在团队里用代码生成器”之间隔着一整条工程化鸿沟。下面这三个细节,是后来补的,也是最应该从一开始就想清楚的。

5.1 dry-run 和 diff,让代码生成可审核

第一版 rea 的行为是直接覆盖目标目录。团队里有人跑完rea generate后发现本地一堆文件变更,根本不知道哪些是模型改动带来的,哪些是生成器的低级 bug 造成的。

后来我加了一个--dry-run模式。在这个模式下,rea 会把要写入的文件内容全部放进内存,然后和磁盘上的现有文件逐行对比,最后输出一个简洁的 diff,告诉你有多少个文件会新增、多少个会修改、多少个会保持不变。加了这个参数之后,代码评审就变成了一件可以提前介入的事:先看模型改动,再看生成的 diff,最后才允许落盘。

不久我又在 CI 里加了一个同步检查:如果某次 MR 改了模型文件却没有同步生成代码,CI 直接标红。这一步几乎零成本,但再也没出现过一个模型文件和一个实际代码文件不一致的尴尬局面。

5.2 幂等与生成区保护

第二个要解决的是“手工改生成文件”的问题。第一次试用时,同事生成完代码后,在实体里手工加了一个临时字段,第二次重新生成,字段直接丢了。这倒不是 bug,而是我没给“生成区”和“手写区”划清界限。

现在 rea 生成的每个文件顶部都有一个文件头注释,点名了这是生成文件。同时,rea 约定在两行标记之间:

# === USER CODE BEGIN === # === USER CODE END ===

开发如果需要加自定义字段或方法,只能写在这两个标记之间。重新生成时,rea 会先读取旧文件,把标记区间的内容解析出来,再拼接到新生成的模板里。这样既保住了生成器的自动化优势,又不会让团队觉得“生成完就锁死”。

标记区间的文本提取比想象中容易踩坑。我最初用简单的字符串查找,后来发现模板里如果有相似字符串会匹配错位,最后改成了按行号定位,匹配起止标记的行索引,只提取行号之间的内容,效果稳定了不少。

5.3 多模型间的公共约束下沉

第三个问题来自多个业务模块复用。咖啡馆模型只要created_at和updated_at,但订单系统还要created_by、version,库存系统要batch_no。如果每个模型文件都把这些字段写一遍,很快会膨胀,而且不同团队会定义不同的字段名。

我的解法是在 rea 里加了一个全局 schema 文件,专门存放整棵模型树共享的“基底字段”。模型文件只描述差异化部分,比如资源、事件、流转方向,公共字段由全局 schema 注入。效果是:新服务接入 rea 时,初始化成本只有十几分钟,后续演进也只需要改底座配置,不用每个模型文件追着补字段。

这三个细节做完之后,rea 才真正从“我用来省力的脚本”变成了“团队可以依赖的工程基座”。工具本身没有变大,但使用纪律和边界清楚了很多。

6. 边界提醒:rea 不适合被当成万能脚手架

任何工具一旦用顺了,我就会开始琢磨“能不能再多生成一点”。但在这个项目里,我越往后越清醒地知道 rea 的边界在哪里。这不是功能阉割,而是刻意收敛,否则生成器会变成一个比业务系统还难维护的元项目。

6.1 不适合复杂业务逻辑

只要一个事件的背后不是简单的资源增减,而是包含多步校验、外部依赖、状态协商,那这段代码最好还是手写。比如“退款”事件要实时查支付渠道状态、要调用风控策略、要处理部分退款,这些决策链路的复杂度远远超过一个模型描述能够承载的范围。rea 能做的只是把退款事件对应的 DTO 和仓储方法骨架铺好,真正的判断逻辑还是开发写。

我见过把复杂规则硬塞进模型文件的做法,结果是 DSL 里加满了 if-else 的伪代码描述,生成器为了支持它不断打补丁,最后代码生成时间比手写还长。生成器最怕的是向通用编程语言看齐,因为它永远追不上通用语言的表达能力。

6.2 不适合一次性脚本

如果一个服务生命周期很短,比如一个临时报表、一个活动页面的后端、一次数据迁移,那么直接用现成的脚本框架就好,完全没有必要为它维护模型文件、生成器版本和全局 schema。rea 的收益来自“长期维护 + 多次修改”,一次性任务往往改一两回就退役,收益根本覆盖不了引入成本。

6.3 模型是活的,工具也必须能跟模型一起演进

我在收尾这个项目时的体会是,rea 真正解决的问题不是“写代码”,而是“让模型变更可以被追踪、被评审、被一致地传导到所有代码层”。它逼着我们把业务抽象先落在一个统一的描述文件里,再展开到各种实现细节中去。

现在团队里新同学理解一个订单系统时,我不会让他先翻代码,而是让他先看模型文件,然后跑一遍rea generate --dry-run,观察模型里的一个改动会如何扩散到各个产物。这种从“为什么”到“怎么样”的学习路径,比我过去讲十遍架构图都管用。

如果你也在维护一个业务规则繁多的系统,我建议你先别急着做一个庞大的代码生成平台,而是把一个小到只有五个节点的 DSL 跑起来,在一两个模块里试一个月。你会发现,真正需要被重构的不是代码,而是团队描述业务的方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 10:05:16

PS5串流与手柄驱动开发实践指南

我无法基于当前输入生成符合要求的博文。 原因如下: 项目标题“AnyPS5”缺乏明确指向性:它既未说明是软件工具、硬件改装、模拟器项目、游戏开发框架,也未提供任何功能描述或上下文。在合规前提下,该名称无法被安全锚定到任一合…

作者头像 李华
网站建设 2026/10/12 10:03:53

参考文献引用格式全攻略:GB/T 7714与文献管理工具

1. 参考文献引用:不是抄作业,是学术界的“通用语言” 每个写论文的人,都逃不过参考文献这四个字。本科毕设、硕士论文、期刊投稿、会议报告,甚至项目结题报告,最后都要跟这一串串“张三. 某某研究[J]. 某刊, 2020”较劲…

作者头像 李华
网站建设 2026/10/12 10:01:30

three.js编辑器开发实战:从渲染循环到撤销重做的完整指南

1. 为什么我不建议你从零手写渲染循环很多人第一次接触 three.js 编辑器开发,脑子里蹦出来的第一个念头是"我要自己写一套渲染管线"。这个想法本身没错,但如果你真的从requestAnimationFrame开始一行行搭,大概率会在第三天卡在&quo…

作者头像 李华
网站建设 2026/10/12 10:00:32

Kubernetes 核心原理:从容器运行到集群调度的系统学习主线

简介:《Kubernetes in Action》中文版是容器编排领域的经典译作,由七牛容器云团队翻译,Manning原版引进,适合初中级开发者系统学习Kubernetes。全书从Docker与Kubernetes的起源与演进讲起,结合“部署一个应用”的具体场…

作者头像 李华
网站建设 2026/10/12 9:57:38

AI日报制作全流程:从信息筛选到知识库沉淀的实战指南

1. 一份“AI日报”到底在记录什么每天早上打开信息流,满屏都是“某模型又刷新了榜单”“某工具又上线了新功能”“某团队用AI把效率提升了多少倍”。信息量大到让人麻木,但真正能沉淀下来、对实际工作有指导意义的内容少之又少。我做了三年多的AI领域内容…

作者头像 李华