告别“大泥球”!理解 DDD 应用服务的设计原则,构建清晰可维护的业务系统
本文已收录在Github,关注我,紧跟本系列专栏文章,咱们下篇再续!
- 🚀 魔都架构师 | 全网30W技术追随者
- 🔧 大厂分布式系统/数据中台实战专家
- 🏆 主导交易系统百万级流量调优 & 车联网平台架构
- 🧠 AIGC应用开发先行者 | 区块链落地实践者
- 🌍 以技术驱动创新,我们的征途是改变世界!
- 👉 实战干货:编程严选网
1 啥是应用层
定义软件要完成的任务,并指挥表达领域概念的对象来解决问题。该层对业务意义重大,也是与其他系统的应用层交互的必要渠道。
要尽量简单,不包含业务规则或知识,而只为下一层中的领域对象协调任务,分配工作,使它们协作。
UML中有用例(Use Case)的概念,表示软件向外提供业务功能的基本逻辑单元。DDD中的业务是第一优先级,自然希望对业务的处理能显现出来,DDD提供称为应用服务(ApplicationService)的抽象层。
ApplicationService采用门面模式,作为领域模型向外提供业务功能的总出入口,就像酒店的前台处理客户的不同需求。
编码实现业务功能时,通常有2种工作流程:
- 自底向上:先设计数据模型,如关系型数据库的表结构,再实现业务逻辑。这种方式将关注点优先放在技术性的数据模型,而不是代表业务的领域模型
- 自顶向下:拿到一个业务需求,先与客户方确定好请求数据格式,再实现Controller和ApplicationService,然后实现领域模型(此时的领域模型通常已经被识别出来),最后实现持久化
DDD自然应采用自顶向下。ApplicationService实现遵循一个简单原则:一个业务用例对应ApplicationService的一个业务方法。
2 电商案例
修改Order中Product的数量的业务需求
实现OrderApplicationService:
@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {
Order order = orderRepository.byId(orderId(id));
order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount());
orderRepository.save(order);
}
OrderController调用OrderApplicationService:
@PostMapping(“/{id}/products”)
public void changeProductCount(@PathVariable(name = “id”) String id, @RequestBody @Valid ChangeProductCountCommand command) {
orderApplicationService.changeProductCount(id, command);
}
此时,order.changeProductCount()和orderRepository.save()都还没有必要实现,但由OrderController和
OrderApplicationService所构成的业务处理的架子已搭建好。
可见,“修改Order中Product的数量”用例中的OrderApplicationService.changeProductCount()方法实现只有几行代码,然而,如此简单的ApplicationService却很多讲究:
3 应用服务设计原则
业务方法与业务用例一一对应
业务方法与事务一一对应
即每个业务方法均构成独立的事务边界 ,案例中,OrderApplicationService.changeProductCount()方法标记有Spring的@Transactional。
不该包含业务逻辑
业务逻辑应该放在领域模型中实现,更准确的说是放在聚合根中实现,本例中,order.changeProductCount()方法才是真正实现业务逻辑的地方,而ApplicationService只是作为代理调用order.changeProductCount(),因此,ApplicationService应是很薄一层。
与UI或通信协议无关
ApplicationService的定位并不是整个软件系统的门面,而是领域模型的门面,这意味着ApplicationService不应该处理诸如UI交互或者通信协议之类的技术细节。在本例中,Controller作为ApplicationService的调用者负责处理通信协议(HTTP)以及与客户端的直接交互。
这种处理方式使得ApplicationService具有普适性,也即无论最终的调用方是HTTP的客户端,还是RPC的客户端,甚至一个Main函数,最终都统一通过ApplicationService才能访问到领域模型。
接受原始数据类型:ApplicationService作为领域模型的调用方,领域模型的实现细节对其来说应该是个黑盒子,因此ApplicationService不应该引用领域模型中的对象。此外,ApplicationService接受的请求对象中的数据仅仅用于描述本次业务请求本身,在能够满足业务需求的条件下应尽量简单。因此,ApplicationService通常处理一些比较原始的数据类型。在本例中,OrderApplicationService所接受的Order ID是Java原始的String类型,在调用领域模型中的Repository时,才被封装为OrderId对象。
4 用户登录案例
用户登录时序图:
之前这里的处理有一定问题,没有保证此段代码可靠性和事务性,一旦处理过程失败,用户可能就无法获得退款,用户体验差。应放到调度器里执行。
AppTradeService.java
public void onDeviceFailure(DeviceFailureEvent event) {
if (event.getMachineType() == VendingMachineType.SLOT) {
SlotVendingMachine machine = machineRepository
.getSlotVendingMachineById(event.getMachineId());
if (machine.getState() == SlotVendingMachineState.Trading
&& machine.getCurOrder().getOrderId() == event.getOrderId()) {
machine.cancelOrder();
} else {
Order order = orderRepository.getOrderById(event.getOrderId());
order.cancel();
}
}
}
重构如下:
public void onDeviceFailure(DeviceFailureEvent event) {
if (event.getMachineType() == VendingMachineType.SLOT) {
Map<String, Object> params = Maps.newHashMap();
params.put("event", event);
scheduler.scheduleRetry(DeviceFailureExecutor.class, params, 0, 1000);
}
}
5 对比
| 类型 | 核心职责 | 说明 |
|---|---|---|
| 应用服务 | 事务控制访问权限任务调度调用领域层 | 所有协调性工作,不能包含业务逻辑 |
| 领域服务 | 业务逻辑 | 只含“无处安放”的业务逻辑 |
6 总结
应用层是调用领域模型完成用户需求的地方。应用层的实现:
- 事务
- 鉴权Spring Security、JWT
- 任务调度quartz
FAQ
Q:ddd考虑domainservice和应用service区别
Q:啥时应用service直接调用 repository?
在领域驱动设计(DDD)中,ApplicationService可能会直接调用Repository而非DomainService的情况通常包括以下几种:
- 简单的CRUD操作:当应用层需要执行简单的创建、读取、更新或删除操作时,可以直接通过
Repository与数据库进行交互,无需复杂的领域逻辑。 - 查询操作:当应用服务需要执行查询操作来获取数据,而这些数据不需要经过领域逻辑处理时,可以直接使用
Repository来实现。 - 事务管理:在需要管理事务的情况下,
ApplicationService可能会直接调用Repository来确保操作的原子性。通常,ApplicationService会启动一个事务,执行多个Repository调用,并在成功后提交事务。
以下是一些具体场景:
- 直接数据访问:如果操作仅仅是获取或保存领域对象,而不涉及任何业务规则或逻辑,
ApplicationService可以直接调用Repository。 - 无领域逻辑:在某些情况下,可能没有定义对应的
DomainService,因为操作不需要复杂的业务逻辑处理。 - 性能优化:在需要优化性能时,可能会选择直接通过
Repository进行数据操作,避免额外的服务调用开销。 - 编排操作:
ApplicationService可能需要编排多个简单的数据访问操作,这些操作可能不需要通过DomainService。
以下是一个示例:
public class CustomerApplicationService {
private final CustomerRepository customerRepository;
public CustomerApplicationService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public Customer findCustomerById(String customerId) {
// 直接通过Repository查询客户信息,不涉及领域逻辑
return customerRepository.findById(customerId);
}
public void createCustomer(CreateCustomerCommand command) {
// 启动事务
// 创建客户实体,并直接保存到数据库
Customer customer = new Customer(command.getCustomerId(), command.getName());
customerRepository.save(customer);
// 提交事务
}
}
在这个例子中,findCustomerById方法直接通过Repository查询客户信息,没有复杂的业务逻辑需要处理,因此没有必要通过DomainService。同样,createCustomer方法直接通过Repository保存新的客户实体,尽管这可能涉及到简单的验证逻辑,但它通常不足以需要DomainService的介入。

浙公网安备 33010602011771号