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

聚合根之间的引用通过ID完成:在聚合根边界设计合理的情况下,一次业务用例只会更新一个聚合根,此时你在该聚合根中去引用另外聚合根的整体有什么好处呢?在本文示例中,一个Order下的OrderItem引用了ProductId,而不是整个Product。

聚合根内部的所有变更都必须通过聚合根完成:为了保证聚合根的一致性,同时避免聚合根内部逻辑向外泄露,客户方只能将整个聚合根作为统一调用入口。

如果一个事务需要更新多个聚合根,首先思考一下自己的聚合根边界处理是否出了问题,因为在设计合理的情况下通常不会出现一个事务更新多个聚合根的场景。如果这种情况的确是业务所需,那么考虑引入消息机制和事件驱动架构,保证一个事务只更新一个聚合根,然后通过消息机制异步更新其他聚合根。

聚合根不应该引用基础设施。

外界不应该持有聚合根内部的数据结构。

尽量使用小聚合。

实体 vs 值对象

软件模型中存在实体对象(Entity)和值对象(Value Object)之说,这种划分方式事实上并不是DDD的专属,但是在DDD中我们非常强调这两者之间的区别。

实体对象表示的是具有一定生命周期并且拥有全局唯一标识(ID)的对象,比如本文中的Order和Product,而值对象表示用于起描述性作用的,没有唯一标识的对象,比如Address对象。

聚合根一定是实体对象,但是并不是所有实体对象都是聚合根,同时聚合根还可以拥有其他子实体对象。聚合根的ID在整个软件系统中全局唯一,而其下的子实体对象的ID只需在单个聚合根下唯一即可。 在本文示例项目中,OrderItem是聚合根Order下的子实体对象:

public class OrderItem { private ProductId productId; private int count; private BigDecimal itemPrice; }

可以看到,虽然OrderItem使用了ProductID作为ID,但是此时我们并没有享受ProductID的全局唯一性,事实上多个Order可以包含相同ProductID的OrderItem,也即多个订单可以包含相同的产品。

区分实体和值对象的一个很重要的原则便是根据相等性来判断,实体对象的相等性是通过ID来完成的,对于两个实体,如果他们的所有属性均相同,但是ID不同,那么他们依然两个不同的实体,就像一对长得一模一样的双胞胎,他们依然是两个不同的自然人。对于值对象来说,相等性的判断是通过属性字段来完成的。比如,订单下的送货地址Address对象便是一个典型的值对象:

public class Address { private String province; private String city; private String detail; @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Address address = (Address) o; return province.equals(address.province) && city.equals(address.city) && detail.equals(address.detail); } @Override public int hashCode() { return Objects.hash(province, city, detail); } }

在Address的equals()方法中,通过判断Address所包含的所有属性(province,city,detail)来决定两个Address的相等性。

值对象还有一个特点是不变的(Immutable),也就说一个值对象一旦被创建出来了便不能对其进行变更,如果要变更,必须重新创建一个新的值对象整体替换原有的。比如,示例项目有一个业务需求:

在订单未支付的情况下,可以修改订单送货地址的详细地址(detail)

由于Address是Order聚合根中的一个对象,对Address的更改只能通过Order完成,在Order中实现changeAddressDetail()方法:

public void changeAddressDetail(String detail) { if (this.status == PAID) { throw new OrderCannotBeModifiedException(this.id); } this.address = this.address.changeDetailTo(detail); }

可以看到,通过调用address.changeDetailTo()方法,我们获取到了一个全新的Address对象,然后将新的Address对象整体赋值给address属性。此时Address.changeDetailTo()的实现如下:

public Address changeDetailTo(String detail) { return new Address(this.province, this.city, detail); }

这里的changeDetailTo()方法使用了新的详细地址detail和未发生变更的province、city重新创建出了一个Address对象。

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

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