看看这个有效的总体设计 http://dddcommunity.org/library/vernon_2011/弗农的三篇系列文章。我发现它们对于了解何时以及如何设计较小的聚合而不是大型集群聚合非常有用。
EDIT
我想举几个例子来改进我之前的答案,请随意分享您的想法。
首先,关于聚合的快速定义(摘自领域驱动设计的模式、原则和实践斯科特·米勒所著)
实体和值对象协作形成满足领域模型内的不变量的复杂关系。当处理对象的大型互连关联时,通常很难确保对域对象执行操作时的一致性和并发性。领域驱动设计具有聚合模式来确保一致性并定义对象图的事务并发边界。大型模型按不变量进行分割,并分组为实体和值对象的聚合,这些实体和值对象被视为概念整体。
让我们通过一个例子来看看实践中的定义。
简单的例子
第一个示例展示了定义聚合根如何帮助确保对域对象执行操作时的一致性。
鉴于下一个业务规则:
获胜的拍卖出价必须始终在拍卖结束之前进行。如果在拍卖结束后进行中标,则该域将处于无效状态,因为不变量已被破坏并且模型无法正确应用域规则。
这里有一个由拍卖和出价组成的聚合,其中拍卖是聚合根。
如果我们说 Bid 也是一个单独的聚合根,那么您将有一个BidsRepository
,你可以轻松地做到:
var newBid = new Bid(money);
BidsRepository->save(auctionId, newBid);
并且您在未通过定义的业务规则的情况下保存了出价。但是,将拍卖作为唯一的聚合根,您将强制执行您的设计,因为您需要执行以下操作:
var newBid = new Bid(money);
auction.placeBid(newBid);
auctionRepository.save(auction);
因此,您可以检查方法内的不变量placeBid
如果他们想进行新的出价,任何人都不能跳过它。
在这里很明显,出价的状态取决于拍卖的状态。
复杂的例子
回到与客户相关联的订单示例,看起来没有不变量使我们定义一个由客户及其所有订单组成的巨大聚合,我们可以通过标识符引用来保持两个实体之间的关系。通过这样做,我们可以避免在获取客户时加载所有订单,并减轻并发问题。
但是,假设现在业务定义了下一个不变量:
我们希望为客户提供一个口袋,以便他们可以充钱来购买产品。因此,如果客户现在想要购买产品,则需要有足够的资金才能购买。
这么说来,pocket 是 Customer Aggregate Root 中的一个 VO。现在看来,拥有两个独立的聚合根(一个用于客户,另一个用于订单)并不是满足新不变量的最佳选择,因为我们可以在不检查规则的情况下保存新订单。看来我们被迫将客户视为根本。这将影响我们的性能、可扩展性和并发问题等。
解决方案?最终一致性。如果我们允许客户购买产品怎么办?也就是说,拥有订单的聚合根,因此我们创建订单并保存它:
var newOrder = new Order(customerId, ...);
orderRepository.save(newOrder);
我们在创建订单时发布一个事件,然后异步检查客户是否有足够的资金:
class OrderWasCreatedListener:
var customer = customerRepository.findOfId(event.customerId);
var order = orderRepository.findOfId(event.orderId);
customer.placeOrder(order); //Check business rules
customerRepository.save(customer);
如果一切顺利,我们就满足了不变量,同时保持了我们一开始想要的设计,每个请求仅修改一个聚合根。否则,我们将向客户发送电子邮件,告知其资金不足的问题。我们可以通过添加她可以用当前预算购买的电子邮件替代选项来提前实现这一目标,并鼓励她自掏腰包。
考虑到UI可以帮助我们避免客户没有足够的钱付款,但我们不能盲目信任UI。
希望您发现这两个示例都很有用,如果您为暴露的场景找到更好的解决方案,请告诉我:-)