后端开发实践系列之二——领域驱动设计(DDD)编码实践 (4)

接地气一点地讲,聚合根(Aggreate Root, AR)就是软件模型中那些最重要的以名词形式存在的领域对象,比如本文示例项目中的Order和Product。又比如,对于一个会员管理系统,会员(Member)便是一个聚合根;对于报销系统,报销单(Expense)便是一个聚合根;对于保险系统,保单(Policy)便是一个聚合根。聚合根是主要的业务逻辑载体,DDD中所有的战术实现都围绕着聚合根展开。

然而,并不是说领域模型中的所有名词都可以建模为聚合根。所谓“聚合”,顾名思义,即需要将领域中高度内聚的概念放到一起组成一个整体。至于哪些概念才能聚到一起,需要我们对业务本身有很深刻的认识,这也是为什么DDD强调开发团队需要和领域专家一起工作的原因。近年来流行起来的事件风暴建模活动,究其本意也是通过罗列出领域中发生的所有事件可以让我们全面的了解领域中的业务,进而识别出聚合根。

对于“更新Order中Product数量”用例,聚合根Order的实现如下:

public void changeProductCount(ProductId productId, int count) { if (this.status == PAID) { throw new OrderCannotBeModifiedException(this.id); } OrderItem orderItem = retrieveItem(productId); orderItem.updateCount(count); this.totalPrice = calculateTotalPrice(); } private BigDecimal calculateTotalPrice() { return items.stream() .map(OrderItem::totalPrice) .reduce(ZERO, BigDecimal::add); } private OrderItem retrieveItem(ProductId productId) { return items.stream() .filter(item -> item.getProductId().equals(productId)) .findFirst() .orElseThrow(() -> new ProductNotInOrderException(productId, id)); }

在本例中,Order中的品项(orderItems)和总价(totalPrice)是密切相关的,orderItems的变化会直接导致totalPrice的变化,因此,这二者自然应该内聚在Order下。此外,totalPrice的变化是orderItems变化的必然结果,这种因果关系是业务驱动出来的,为了保证这种“必然”,我们需要在Order.changeProductCount()方法中同时实现“因”和“果”,也即聚合根应该保证业务上的一致性。在DDD中,业务上的一致性被称为不变条件(Invariants)。

还记得上文中提到的“违背内聚性的悬念”吗?当时调用Order上的业务方式如下:

..... order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount()); order.updateTotalPrice(); .....

为了实现“更新Order中Product数量”业务功能,这里先后调用了Order上的两个public方法changeProductCount()和updateTotalPrice()。虽然这种做法也能正确地实现业务逻辑,但是它将保证业务一致性的职责交给了Order的调用方(上文中的Controller)而不是Order自身,此时调用方需要确保在调用了changeProductCount()之后必须调用updateTotalPrice()方法,这一方面是Order中业务逻辑的泄露,另一方面调用方并不承担这样的职责,而Order才最应该承担这样的职责。

对内聚性的追求会自然地延伸出聚合根的边界。在DDD的战略设计中,我们已经通过限界上下文的划分将一个大的软件系统拆分为了不同的“模块”,在这样的前提下,再在某个限界上下文中来讨论内聚性将比在大泥球系统中讨论变得简单得多。

对聚合根的设计需要提防上帝对象(God Object),也即用一个大而全的领域对象来实现所有的业务功能。上帝对象的背后存在着一种表面上看似合理的逻辑:既然要内聚,那么让我们把所有相关的东西都聚到一起吧,比如用一个Product类来应付所有的业务场景,包括订单、物流、发票等等。这种机械的方式看似内聚,实则恰恰是内聚性的反面。要解决这样的问题依然需要求助于限界上下文,不同限界上下文使用各自的通用语言(Ubiquitous Language),通用语言要求一个业务概念不应该有二义性,在这样的原则下,不同的限界上下文可能都有自己的Product类,虽然名字相同,却体现着不同的业务。

不同的限界上下文中都有各自的Product,有些Product是聚合根,有些不是

除了内聚性和一致性,聚合根还有以下特征:

聚合根的实现应该与框架无关:既然DDD讲求业务复杂度和技术复杂度的分离,那么作为业务主要载体的聚合根应该尽量少地引用技术框架级别的设施,最好是POJO。试想一下,如果你的项目哪天需要从Spring迁移到Play,而你可以自信地给老板说,直接将核心Java代码拷贝过去即可,这将是一种多么美妙的体验。又或者说,很多时候技术框架会有“大步”的升级,这种升级会导致框架中API的变化并且不再支持向后兼容,此时如果我们的领域模与框架无关,那么便可做到在框架升级的过程中幸免于难。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpyjgw.html