可以看到,DAO中多出了很多方法,此时的DAO不再只是对持久化的封装,而是也会包含业务逻辑。另外,DAO.updateTotalPrice(id)方法的实现中将直接调用SQL来实现Order总价的更新。与“Service+贫血模型”方式相似,事务脚本也存在业务逻辑分散的问题。
事实上,事务脚本并不是一种全然的反模式,在系统足够简单的情况下完全可以采用。但是:一方面“简单”这个度其实并不容易把握;另一方面软件系统通常会在不断的演进中加入更多的功能,使得原本简单的代码逐渐变得复杂。因此,事务脚本在实际的应用中使用得并不多。
3. 基于领域对象的实现在这种方式中,核心的业务逻辑被内聚在行为饱满的领域对象(Order)中,实现Order类如下:
public void changeProductCount(ProductId productId, int count) { if (this.status == PAID) { throw new OrderCannotBeModifiedException(this.id); } OrderItem orderItem = retrieveItem(productId); orderItem.updateCount(count); }然后在Controller或者Service中,调用Order.changeProductCount():
@PostMapping("/order/{id}/products") public void changeProductCount(@PathVariable(name = "id") String id, @RequestBody @Valid ChangeProductCountCommand command) { Order order = DAO.byId(orderId(id)); order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount()); order.updateTotalPrice(); DAO.saveOrUpdate(order); }可以看到,所有业务(“检查Order状态”、“修改Product数量”以及“更新Order总价”)都被包含在了Order对象中,这些正是Order应该具有的职责。(不过示例代码中有个地方明显违背了内聚性原则,下文会讲到,作为悬念读者可以先行尝试着找一找)
事实上,这种方式与本文要讲的DDD战术模式已经很相近了,只是DDD抽象出了更多的概念与原则。
基于业务的分包在本系列的上一篇:Spring Boot项目模板文章中,其实我已经讲到了基于业务的分包,结合DDD的场景,这里再简要讨论一下。所谓基于业务分包即通过软件所实现的业务功能进行模块化划分,而不是从技术的角度划分(比如首先划分出service和infrastruture等包)。在DDD的战略设计中,我们关注于从一个宏观的视角俯视整个软件系统,然后通过一定的原则对系统进行子域和限界上下文的划分。在战术实践中,我们也通过类似的提纲挈领的方法进行整体的代码结构的规划,所采用的原则依然逃离不了“内聚性”和“职责分离”等基本原则。此时,首先映入眼帘的便是软件的分包。
在DDD中,聚合根(下文会讲到)是主要业务逻辑的承载体,也是“内聚性”原则的典型代表,因此通常的做法便是基于聚合根进行顶层包的划分。在示例电商项目中,有两个聚合根对象Order和Product,分别创建order包和product包,然后在各自的顶层包下再根据代码结构的复杂程度划分子包,比如对于product包:
└── product ├── CreateProductCommand.java ├── Product.java ├── ProductApplicationService.java ├── ProductController.java ├── ProductId.java ├── ProductNotFoundException.java ├── ProductRepository.java └── representation ├── ProductRepresentationService.java └── ProductSummaryRepresentation.java可以看到,ProductRepository和ProductController等多数类都直接放在了product包下,而没有单独分包;但是展现类ProductSummaryRepresentation却做了单独分包。这里的原则是:在所有类已经被内聚在了product包下的情况下,如果代码结构足够的简单,那么没有必要再次进行子包的划分,ProductRepository和ProductController便是这种情况;而如果多个类需要做再次的内聚,那么需要另行分包,比如通过REST API接口返回Product数据时,代码中涉及到了两个对象ProductRepresentationService和ProductSummaryRepresentation,这两个对象是紧密关联的,因此将他们放在representation子包下。而对于更加复杂的Order,分包如下:
├── order │ ├── OrderApplicationService.java │ ├── OrderController.java │ ├── OrderPaymentProxy.java │ ├── OrderPaymentService.java │ ├── OrderRepository.java │ ├── command │ │ ├── ChangeAddressDetailCommand.java │ │ ├── CreateOrderCommand.java │ │ ├── OrderItemCommand.java │ │ ├── PayOrderCommand.java │ │ └── UpdateProductCountCommand.java │ ├── exception │ │ ├── OrderCannotBeModifiedException.java │ │ ├── OrderNotFoundException.java │ │ ├── PaidPriceNotSameWithOrderPriceException.java │ │ └── ProductNotInOrderException.java │ ├── model │ │ ├── Order.java │ │ ├── OrderFactory.java │ │ ├── OrderId.java │ │ ├── OrderIdGenerator.java │ │ ├── OrderItem.java │ │ └── OrderStatus.java │ └── representation │ ├── OrderItemRepresentation.java │ ├── OrderRepresentation.java │ └── OrderRepresentationService.java