当前位置: 首页 > news >正文

Wow 获 KaiCode’26 Excellent Award:DDD/CQRS 如何落到可测试代码? - Ahoo

Wow:模型即服务

感谢 KaiCode’26 对 Wow 的认可,也感谢一路参与 Wow 的贡献者和使用者。

这篇文章想回到 Wow 本身,回答一个长期困扰 DDD 实践者的问题:

一个主打 DDD、CQRS 和 Event Sourcing 的框架,怎样证明自己不是“架构概念展览馆”?

我的答案是:看它能否把复杂性从业务代码中拿走,同时又不把复杂性藏进一个无法测试、无法观察的黑盒。

本文不打算再列一遍功能清单,而是用 Wow 仓库里真实的购物车代码,拆开它的核心理念:模型即服务(Domain Model as a Service)

一、DDD 最大的问题,往往不是不会画图

很多团队第一次实践 DDD,最后得到的代码结构大致是:

Controller-> ApplicationService-> DomainService-> Repository-> ORM

目录变多了,类变多了,业务规则却依然散落在参数校验、Service 条件分支和数据库更新语句里。

这不是分层本身有问题,而是我们经常把“业务能力”实现成一次数据库状态修改:

update cart_item
set quantity = quantity + 1
where cart_id = ? and product_id = ?;

这条 SQL 能告诉我们现在的数量,却没有回答三个更重要的问题:

  1. 谁发起了什么意图?
  2. 当时为什么允许这次修改?
  3. 这次修改产生了什么业务事实?

在 Wow 中,这三个问题分别对应 Command、聚合规则和 Domain Event

传统分层中的偶然复杂性

二、先看一段真实的领域模型

下面的代码来自 Wow 当前仓库的购物车示例(正文略去了与主题无关的部分):

@StaticTenantId
@AggregateRoot
@AggregateRoute(owner = AggregateRoute.Owner.AGGREGATE_ID)
class Cart(private val state: CartState) {@OnCommand(returns = [CartItemAdded::class, CartQuantityChanged::class])fun onCommand(command: AddCartItem): Any {require(state.items.size < MAX_CART_ITEM_SIZE) {"购物车最多只能添加[$MAX_CART_ITEM_SIZE]个商品."}state.items.firstOrNull { it.productId == command.productId }?.let {return CartQuantityChanged(changed = it.copy(quantity = it.quantity + command.quantity))}return CartItemAdded(added = CartItem(productId = command.productId,quantity = command.quantity))}
}

这段代码只做两件事:

  • 根据当前状态校验业务规则;
  • 返回一个表示“已经发生什么”的事件。

它没有注入 Repository,没有打开事务,也没有直接修改 CartState。如果商品已存在,产生 CartQuantityChanged;否则产生 CartItemAdded

状态变化在另一个确定性的入口完成:

class CartState(val id: String) {var items: List<CartItem> = listOf()private set@OnSourcingfun onCartItemAdded(event: CartItemAdded) {items = items + event.added}@OnSourcingfun onCartQuantityChanged(event: CartQuantityChanged) {items = items.map {if (it.productId == event.changed.productId) event.changed else it}}
}

onCommand 负责决策,onSourcing 负责把已经发生的事实应用到状态。这条边界非常重要:

  • 与状态有关的业务决策放在命令处理阶段;
  • onSourcing 只应用事件,不做业务校验、外部调用或其他副作用;
  • 在同一模型版本与兼容策略下,同一串事件应确定性地得到相同状态。

这才是 Event Sourcing 的可维护性基础,而不是简单地“把数据库表换成事件表”。

Command、Aggregate、Event 与 State

三、Wow 的核心理念:模型即服务

有了上面的聚合模型之后,传统项目里仍然要手写不少胶水:Controller、路由、参数校验、命令分发、事件持久化、OpenAPI 描述……

Wow 的做法是让 wow-compiler 在编译期扫描 @AggregateRoot@OnCommand@OnEvent 等声明,生成命令与处理器映射、事件处理器元数据和 WebFlux/OpenAPI 路由所需的元数据。

换句话说,Wow 不是让你在领域模型旁边再搭建一套“服务层”,而是把模型直接物化为服务能力:

  1. 模型定义能力:Command 表达意图,聚合规则负责决策,Domain Event 记录事实;
  2. 模型生成入口:编译期元数据驱动 WebFlux 路由与 OpenAPI,无需重复编写 Controller;
  3. 模型驱动状态:事件被持久化、发布并用于重建聚合状态;
  4. 模型可以验证:Given → When → Expect 直接测试业务决策、事件和最终状态。

Wow:Domain Model as a Service

运行时的主链路可以简化为:

HTTP 请求-> 自动注册的 WebFlux 路由-> CommandGateway-> 加载快照与历史事件-> 聚合根处理 Command-> 追加 Domain Event-> 发布事件-> Projection / Saga / EventHandler

Wow Architecture

这并不意味着框架“消灭了复杂性”。持久化、幂等、路由、并发控制、事件发布和故障恢复仍然存在,只是它们被放回了框架和基础设施层,不再要求每个业务功能重复实现一遍。

这就是 Wow 所说的“模型即服务”:

模型不是藏在 Service 和 Repository 后面的内部对象;模型本身就是业务能力的定义,框架负责把它变成可调用、可持久化、可观察、可测试的服务。

四、CQRS 最尴尬的“等一秒再刷新”,应该由协议解决

CQRS 读写分离后,一个常见问题是:命令已经成功,但投影还没更新。很多系统最后写出类似代码:

await submitCommand();
await sleep(1000);
await refreshQuery();

这段代码既不可靠,也不可观测。投影 100 毫秒完成时白等,1.2 秒完成时仍然读到旧数据。

Wow 把命令处理过程定义为一组可等待阶段:

  • SENT:命令已发送;
  • PROCESSED:聚合已处理并产生结果;
  • SNAPSHOT:快照已生成;
  • PROJECTED:符合等待条件的目标投影已完成;
  • EVENT_HANDLED:指定事件处理器已完成;
  • SAGA_HANDLED:指定 Saga 已完成。

CQRS:用明确阶段替代固定延迟

HTTP 客户端可以通过请求头表达自己真正需要的一致性边界。比如,等待示例服务中的 OrderProjector 完成:

Command-Wait-Stage: PROJECTED
Command-Wait-Context: example-service
Command-Wait-Processor: OrderProjector

只指定 PROJECTED 时,Wow 默认匹配当前上下文中符合条件的投影信号;存在多个投影时,可以继续通过 Command-Wait-ContextCommand-Wait-ProcessorCommand-Wait-Function 精确指定等待目标。

客户端不是猜一个延迟,而是在等待一个明确的业务处理信号。对于只关心吞吐量的写入,可以等待 SENT;对于提交后立即查询的交互,可以等待目标 PROJECTED 信号。

这是一个很小的 API 设计,却直接改善了 CQRS 的使用体验。

五、架构好不好,测试最诚实

如果一个领域模型必须启动 Spring、Kafka、MongoDB 才能验证业务规则,那么它的边界大概率还不够干净。

Wow 的测试 DSL 使用 Given → When → Expect 描述聚合行为:

class CartSpec : AggregateSpec<Cart, CartState>({on {givenOwnerId(generateGlobalId())whenCommand(AddCartItem(productId = "productId", quantity = 1)) {expectNoError()expectEventType(CartItemAdded::class)expectState {items.assert().hasSize(1)}}}
})

这个测试同时约束了三件事:命令能否被接受、产生什么事件、事件应用后的状态是什么。

Given、When、Expect 领域测试闭环

在撰写本文时,我在当前 8.9.1 代码上执行了:

./gradlew :example-domain:test \--tests "me.ahoo.wow.example.domain.cart.CartSpec"

结果为 BUILD SUCCESSFUL。这不是为了在文章里摆一条绿色日志,而是强调:示例代码必须和项目当前行为一起演进

对框架项目来说,可重复的测试、静态分析和持续集成,比“又支持了一个中间件”更难长期坚持,也更能说明“模型即服务”不是一个只存在于架构图里的口号。

六、什么项目适合 Wow,什么项目不适合?

Wow 更适合这些场景:

  • 业务规则复杂,状态流转需要被明确建模;
  • 审计、追溯、事件回放是核心需求;
  • 写模型和查询模型有不同的伸缩方式;
  • 存在跨聚合、跨服务的 Saga 与最终一致性流程;
  • 团队愿意用 Command / Event 语言讨论业务。

它不一定适合:

  • 只有少量表单和后台 CRUD 的系统;
  • 团队并不需要事件历史,却愿意为 Event Sourcing 支付全部认知成本;
  • 业务边界尚未形成,只想先用框架“替自己完成建模”。

框架可以降低 DDD 与 Event Sourcing 的工程成本,但不能替团队识别限界上下文,也不能替产品负责人说清业务规则。

写在最后

KaiCode’26 的认可是一份鼓励,但 Wow 真正想长期坚持的仍然是“模型即服务”。判断这件事有没有做到,可以看几个问题:

  • 领域模型是否仍然是代码的中心?
  • 编译期自动化有没有侵蚀可理解性?
  • 测试能否覆盖真实业务行为?
  • 一致性、失败与延迟是否对调用方可见?
  • 文档和协作流程是否配得上代码质量?

奖项会过去,这些问题不会。

如果你正在评估 DDD、CQRS 或 Event Sourcing,不妨先从购物车这样的小聚合开始:写一个 Command,产生一个 Event,用 Given → When → Expect 验证它。等模型能够清晰表达业务之后,再讨论 Kafka、MongoDB、水平扩容和微服务。

这通常比先画一张宏大的架构图更接近正确的起点。

你的项目是怎样处理 CQRS 写后读一致性的?是固定延迟、轮询、事件通知,还是其他方案?欢迎在评论区分享实践和踩过的坑。


相关链接

  • Wow GitHub
  • Wow 中文文档
  • KaiCode’26 官方评审结果
  • 购物车聚合源码
  • 购物车聚合测试

说明:本文由 AI 辅助整理,技术事实均基于 Wow 当前仓库、测试、项目文档与 KaiCode 官方结果页核验。

http://www.jsqmd.com/news/1255678/

相关文章:

  • 重庆正规刻章店推荐 省心办理不踩坑 - 跑政通
  • Velprium时间工作空间:提升开发效率的完整指南
  • 2026年中医培训怎么选?中医师承与专长机构实力盘点 - 深度智识库
  • 目前国产替代的导电胶内存颗粒测试治具企业测试精度高
  • 简历里 Agent 做得再花哨,面试为何死在“谁有权改生产库”?
  • 抖店新手一件代发下单工具哪个好?挑选自动采购软件要看哪些功能 - 电商分享
  • 华为CANN架构下LeakyReLU算子的优化与GAN应用
  • Unity游戏皮肤定制全流程:从Shader编写到性能优化实战指南
  • 【博士论文复现】【阻抗建模、验证扫频法】光伏并网逆变器扫频与稳定性分析(包含锁相环电流环)(Matlab代码、Simulink仿真实现)
  • TTS语音合成器 v1.0 绿色版 媲美真人的微软 Edge 引擎配音神器 1.0 - Windows
  • HarmonyOS开发实战:小分享-ProfilePage个人中心——用户信息卡+统计数据+菜单列表
  • 卡地亚维保服务中心地址和服务电话400-883-8097全面公示:2026年7月 无锡卡地亚腕表静置偷停深度排查专业检修指南 - 卡地亚中国售后中心
  • Google三款Flash模型解析:从通用到专用的AI开发实战指南
  • 2026杭州钻石回收门店实力排行,GIA裸钻变现认准这几家无套路 - 资讯洞察员
  • 2026烟台3家高口碑婚纱摄影品牌横向对比,蓝墨胜在哪? - 生活测评君
  • 数字电源PMBus寄存器配置实战:以TI TPSM846C23为例详解电压裕量与保护功能
  • 目前国产替代的内存颗粒测试夹具制造厂家接触稳定性远超同行业标准
  • Python super()方法深度解析:从MRO原理到多重继承实战
  • 高性能SAR ADC评估套件实战指南:从硬件配置到性能分析
  • GraphRAG 上线最先崩的不是准确率,是团队接不住的权限黑洞与日志缺失
  • OpenCut龙虾助手:AI对话式无痕文本编辑技术解析
  • 优秀教师评选投票制作教程,校内教职工评比活动指南 - 微信投票小程序
  • 家人们,Claude订阅被拒了3次,最后竟然用微信搞定了?!
  • Unity游戏模组开发入门:BepInEx插件框架原理与实践指南
  • 免费AI数字人平台对比:哪些真的能用,哪些是噱头智商税?
  • 塔城黄金回收正规军在哪?认准永兴、昌盛、天乐三家合规连锁门店 - 黄金珠宝
  • 夫妻车辆解押,委托公证书线上办理方法 - 跑政通
  • Claude Code 实战到底解决了什么问题?
  • 【仅限本周开放】2024 Q2 AI推理能力稀缺性报告(含Transformer架构层级优化潜力雷达图+厂商未公开的FP8支持进度表)
  • Zotero Duplicates Merger:终极高效的文献去重解决方案