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

创建聚合根通常通过设计模式中的工厂(Factory)模式完成,这一方面可以享受到工厂模式本身的好处,另一方面,DDD中的Factory还具有将“聚合根的创建逻辑”显现出来的效果。

创生之柱——恒星诞生的地方,距地球约6500光年,由哈勃太空望远镜于1995年拍摄

聚合根的创建过程可简单可复杂,有时可能直接调用构造函数即可,而有时却存在一个复杂的构造流程,比如需要调用其他系统获取数据等。通常来讲,Factory有两种实现方式:

直接在聚合根中实现Factory方法,常用于简单的创建过程

独立的Factory类,用于有一定复杂度的创建过程,或者创建逻辑不适合放在聚合根上

让我们先演示一下简单的Factory方法,在示例订单系统中,有个业务用例是“创建Product”:

创建Product,属性包括名称(name),描述(description)和单价(price),ProductId为UUID

在Product类中实现工厂方法create():

public static Product create(String name, String description, BigDecimal price) { return new Product(name, description, price); } private Product(String name, String description, BigDecimal price) { this.id = ProductId.newProductId(); this.name = name; this.description = description; this.price = price; this.createdAt = Instant.now(); }

这里,Product中的create()方法并不包含创建逻辑,而是将创建过程直接代理给了Product的构造函数。你可能觉得这个create()方法有些多此一举,然而这种做法的初衷依然是:我们希望将聚合根的创建逻辑突显出来。构造函数本身是一个非常技术的东西,任何地方只要涉及到在计算机内存中新建对象都需要使用构造函数,无论创建的初始原因是业务需要,还是从数据库加载,亦或是从JSON数据反序列化。因此程序中往往存在多个构造函数用于不同的场景,而为了将业务上的创建与技术上的创建区别开来,我们引入了create()方法用于表示业务上的创建过程。

“创建Product”所设计到的Factory的确简单,让我们再来看看另外一个例子:“创建Order”:

创建Order,包含用户选择的Product及其数量,OrderId必须调用第三方的OrderIdGenerator获取

这里的OrderIdGenerator是具有服务性质的对象(即下文中的领域服务),在DDD中,聚合根通常不会引用其他服务类。另外,调用OrderIdGenerator生成ID应该是一个业务细节,如前文所讲,这种细节不应该放在ApplicationService中。此时,可以通过Factory类来完成Order的创建:

@Component public class OrderFactory { private final OrderIdGenerator idGenerator; public OrderFactory(OrderIdGenerator idGenerator) { this.idGenerator = idGenerator; } public Order create(List<OrderItem> items, Address address) { OrderId orderId = idGenerator.generate(); return Order.create(orderId, items, address); } } 必要的妥协——领域服务

前面我们提到,聚合根是业务逻辑的主要载体,也就是说业务逻辑的实现代码应该尽量地放在聚合根或者聚合根的边界之内。但有时,有些业务逻辑并不适合于放在聚合根上,比如前文的OrderIdGenerator便是如此,在这种“迫不得已”的情况下,我们引入领域服务(Domain Service)。还是先来看一个列子,对于Order的支付有以下业务用例:

通过支付网关OrderPaymentService完成Order的支付。

在OrderApplicationService中,直接调用领域服务OrderPaymentService:

@Transactional public void pay(String id, PayOrderCommand command) { Order order = orderRepository.byId(orderId(id)); orderPaymentService.pay(order, command.getPaidPrice()); orderRepository.save(order); }

然后实现OrderPaymentService:

public void pay(Order order, BigDecimal paidPrice) { order.pay(paidPrice); paymentProxy.pay(order.getId(), paidPrice); }

这里的PaymentProxy与OrderIdGenerator相似,并不适合于放在Order中。可以看到,在OrderApplicationService中,我们并没有直接调用Order中的业务方法,而是先调用OrderPaymentService.pay(),然后在OrderPaymentService.pay()中完成调用支付网关PaymentProxy.pay()这样的业务细节。

到此,再来反观在通常的实践中我们编写的Service类,事实上这些Servcie类将DDD中的ApplicationService和DomainService糅合在了一起,比如在”基于Service + 贫血模型”的实现“小节中的OrderService便是如此。在DDD中,ApplicationService和DomainService是两个很不一样的概念,前者是必须有的DDD组件,而后者只是一种妥协的结果,因此程序中的DomainService应该越少越好。

Command对象

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

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