一次性讲清DDD设计中的CQRS模式
2026/7/5 4:40:06 网站建设 项目流程

在DDD(领域驱动设计)中,应用层的Command和Query是CQRS(命令查询职责分离)模式的核心概念,它们的设计讲究直接影响代码的可维护性和业务表达的清晰度。下面系统梳理一下关键要点。

核心理念:为什么要在应用层区分Command和Query

CQRS的核心原则来自Bertrand Meyer提出的命令查询分离原则(CQS)

  • Command(命令):一个方法如果修改了系统状态,它就是Command。它不应该返回业务数据,只返回操作结果(成功/失败/异步已接收)。
  • Query(查询):一个方法如果返回了数据,它就是Query。它不应该通过直接或间接的手段修改系统状态。

在DDD应用层中,这不仅仅是理论原则,更落地为具体的对象设计规范

Command对象的设计讲究

Command代表调用方明确想让系统执行的操作指令,预期会对系统产生副作用(写操作)。

设计要点
  • 语义化命名:Command的名字必须能清晰表达"意图",而非"动作"。例如PlaceOrderCommand(下单指令)比CreateOrderCommand(创建订单)更有业务含义。
  • 封装操作所需的全部参数:把散落的多个参数收拢到一个Command对象中,避免接口签名膨胀。
  • 不包含业务逻辑:Command本身是Value Object,只携带数据,不包含规则。
  • 可以包含校验逻辑:Command上可以做基础的数据格式校验(如非空、范围),但业务规则校验应交给领域层。
代码示例
// 好的设计:语义清晰,参数内聚publicclassPlaceOrderCommand{@NotNullprivateLonguserId;@NotNullprivateLongitemId;@Min(1)privateIntegerquantity;privateStringchannel;// 渠道}// 不好的设计:参数散落,无语义Result<OrderDO>checkout(LonguserId,LongitemId,Integerquantity,Stringchannel);

Query对象的设计讲究

Query代表调用方明确想查询的数据需求,预期对系统完全不产生副作用(只读操作)。

设计要点
  • 封装查询条件:包括过滤条件、分页参数、排序规则等,统一收拢到一个Query对象中。
  • 命名体现查询意图:如OrderListQueryUserDetailQuery,让人一看就知道查什么。
  • 可以省略的情况:当仅通过单一ID查询时,可以不创建Query对象,直接传ID即可。
代码示例
// 好的设计publicclassOrderListQuery{privateLongsellerId;privateLongitemId;privateOrderStatusEnumstatus;privateintcurrentPage;privateintpageSize;}// 不好的设计:一个查询条件一个方法,接口膨胀List<OrderDO>queryByItemId(LongitemId);List<OrderDO>queryBySellerId(LongsellerId,intpage,intsize);List<OrderDO>queryByStatus(OrderStatusEnumstatus);

Command vs Query vs DTO 的区别

这是很多人容易混淆的地方:

对比维度Command / QueryDTO
角色应用服务的输入应用服务的输出
语义携带明确的"意图"纯粹的数据容器
是否包含逻辑可包含基础校验不包含任何逻辑(贫血对象)
数量特征理论上可以无限多,每个代表不同意图通常与展示场景对应

应用层服务的设计规范

基于Command和Query的区分,应用服务(ApplicationService)的接口设计应遵循以下规范:

publicinterfaceOrderApplicationService{// Command:写操作,入参是Command对象OrderDTOplaceOrder(@ValidPlaceOrderCommandcommand);// Command:写操作voidcancelOrder(@ValidCancelOrderCommandcommand);// Query:读操作,入参是Query对象List<OrderDTO>queryOrders(OrderListQueryquery);// Query:单一ID查询,可以省略Query对象OrderDTOgetOrder(LongorderId);}

关键规则:

  • 应用服务的入参只能是一个Command、Query或Event对象(单一ID查询除外)
  • 应用服务本身不包含业务逻辑,只负责流程编排
  • Command方法不应返回业务数据,Query方法不应修改状态

为什么要这样设计

解决接口膨胀问题

传统写法中,每增加一个查询条件就要新增一个方法,导致接口无限膨胀。用Query对象封装后,新增查询条件只需在Query对象中加字段,接口签名不变。

提升代码的语义表达力

placeOrder(PlaceOrderCommand cmd)createOrder(Long userId, Long itemId, Integer quantity)更能表达业务意图。代码即文档。

为CQRS架构打下基础

当系统复杂度上升时,Command和Query的分离可以自然演进为物理上的读写分离:

  • Command侧:走领域模型,通过聚合根处理业务逻辑,保证ACID
  • Query侧:绕过领域模型,直接查询为读取优化的数据源(甚至可以用Elasticsearch等异构存储)

三种CQRS架构方案的选择

根据业务复杂度,可以选择不同层次的CQRS实现:

方案存储适用场景复杂度
共享存储/共享模型同一数据库、同一模型简单业务,读写需求差异不大
共享存储/分离模型同一数据库、不同模型中等复杂度,查询需要扁平化数据
分离存储/分离模型不同数据库(如MySQL + ES)高复杂度,读写性能要求差异大

实际项目中,大多数团队会优先落地"读写分离版CQRS"(方案2),而不是上Event Sourcing全家桶。如果你的目标是"让系统边界清晰、查询性能更好、代码职责更干净",从读写分离开始通常是更稳妥的选择。

总结

应用层Command和Query的核心讲究可以浓缩为三句话:

  • Command表达"意图":封装写操作的全部参数,语义清晰,代表"我要系统做什么"
  • Query表达"需求":封装读操作的全部条件,代表"我要看什么数据"
  • 两者严格隔离:Command不改返回值,Query不改状态,这条边界一旦守住,系统就能自然地演进到CQRS架构

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

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

立即咨询