DDD中如何为聚合模型减负?


业务需求场景:

  • 商品应在提交订单时为客户保留。
  • 仅仅将商品添加到购物车并不能保证产品的可用性。
  • 客户不能将已经缺货的产品加入购物车。

事实上,这并不是什么花哨的要求。我曾经做过一个电子商务项目,就有这样的功能。当深入研究领域驱动设计时,我开始思考如何使用 DDD 构建块正确满足这一要求。

”客户不能将已经缺货的产品加入购物车“
这听起来像是一个直观的不变性(banq注:逻辑一致性、约束,“不能”两个字代表了业务规则):

简单直观实现:创建一个 Inventory::CheckAvailability函数方法以响应外部命令,这个函数方法存在于 Inventory::InventoryEntry 聚合根中:

def check_availability!(desired_quantity)
  return unless stock_level_defined?
  raise InventoryNotAvailable if desired_quantity > availability
end

事实上,这个函数方法对聚合的内部状态没有任何作用:

  • 如果产品缺货,此方法只会引发错误。 这是一个糟糕的命令执行者。
  • 它混淆了聚合的代码,聚合内部代码应该保持简约,并且在系统内没有进行任何更改。

外部命令进入 Inventory::CheckAvailability函数方法后,除了读取之外什么也没做时,我开始在读取模型中寻找解决方案。

直接在聚合根上检查可用性后立即下订单并不能保证成功。检查后仅 1 毫秒,它可能会发生变化。因为该命令不能影响聚合的状态。

因此,可以准备了产品可用性读取模型(ProductsAvailability read model),它订阅 Inventory::AvailabilityChanged 事件。我用它来验证调用 Ordering::AddItemToBasket 命令是否有意义。

def add_item
  if Availability::Product.exists?(["uid = ? and available < ?", params[:product_id], params[:quantity]])
    redirect_to edit_order_path(params[:id]),
                alert:
"Product not available in requested quantity!" and return
  end
  command_bus.(Ordering::AddItemToBasket.new(order_id: params[:id], product_id: params[:product_id]))
  head :ok
end

经验教训:

  • 开始区分与系统内某些状态变化相关的硬性业务规则。事实上,这类需求是聚合不变式的良好候选者。
  • 注意到有些需求可以改善用户体验,但对聚合设计的影响并不那么关键。在检查这些需求时,我们并不关心与写入方 100% 的一致性。
  • 有一些读取模型对于读取目的并不严格,这也是可以的。