尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

Wow 获 KaiCode’26 Excellent Award:DDD/CQRS 如何落到可测试代码? - Ahoo
📅 发布时间:2026/7/24 17:41:50
Wow 获 KaiCode’26 Excellent Award:DDD/CQRS 如何落到可测试代码?Wow 获 KaiCode’26 Excellent Award。本文基于 8.9.1 真实代码,拆解核心理念“模型即服务”:领域模型如何定义业务能力,并被框架转化为可调用、可持久化、可观察、可测试的服务。

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-Context、Command-Wait-Processor 和 Command-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 官方结果页核验。

作者:Ahoo Wang (阿虎)

Github: https://github.com/Ahoo-Wang/

SmartSql(高性能、高生产力,超轻量级的ORM!): https://github.com/Ahoo-Wang/SmartSql

SmartCode(不只是代码生成器!): https://github.com/Ahoo-Wang/SmartCode

CoSky 高性能、低成本微服务治理平台 : https://github.com/Ahoo-Wang/CoSky

CosId 通用、灵活、高性能的分布式 ID 生成器 : https://github.com/Ahoo-Wang/CosId

Wow 基于 DDD、EventSourcing 的现代响应式 CQRS 架构微服务开发框架: https://github.com/Ahoo-Wang/Wow

CoSec 基于 RBAC 和策略的多租户响应式安全框架: https://github.com/Ahoo-Wang/CoSec


本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

相关新闻

  • 重庆正规刻章店推荐 省心办理不踩坑 - 跑政通
  • 目前国产替代的导电胶内存颗粒测试治具企业测试精度高
  • 2026烟台3家高口碑婚纱摄影品牌横向对比,蓝墨胜在哪? - 生活测评君

最新新闻

  • 如何快速备份QQ空间:5分钟完成数据备份的完整指南
  • AI 提示词工程(prompt engineering)
  • 重新定义前端构建速度:深度解析 SWC 如何用 Rust 颠覆 JavaScript 工具链
  • AI生成视频完播率提升217%的5步拆解法,头部MCN内部培训文档首度流出,限前200名领取?
  • 2026 年综合表现稳定的原木全屋定制厂家参考盘点,家装业主可对比参考 - 起跑123
  • 百川智能联创全员出走,王小川All in医疗AI能否破局?

日新闻

  • 武汉卡地亚LOVE钻戒与钻石项链回收变现攻略|多家门店行情参考 - 大牌深度测评
  • 2026年无锡地区健康管理如何考量?四家机构业务体系概览
  • 2026图片去水印软件哪个好用 手机电脑免费工具盘点 - 免费软件工具方法教程

周新闻

  • SaaS软件行业GEO实践:AI搜索时代的品牌可见性与获客新路径
  • 什么是PCTFE?医药高端包装的“防潮王牌“材料
  • 【JVM调优实战】16-可视化利器-JConsole-VisualVM-JMC

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号