本文已收录在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 用户登录案例
用户登录时序图:
sequenceDiagram
box Purple 用户侧
actor 用户
participant 微信小程序
end
box Gray 内部服务
participant 交易上下文
participant 用户上下文
end
box Blue
participant 微信平台服务
end
Note right of 微信平台服务: 含登录、用户信息、支付等接口服务
用户->>微信小程序: 扫描货柜机二维码打开
微信小程序->>+交易上下文: 打开货柜机(柜门机)柜门 <br> <token>
交易上下文-->>-微信小程序: 未认证
微信小程序->>微信小程序: wx.login()
微信小程序->>+用户上下文: 登录smartrm系统 (js_code)
用户上下文->>+微信平台服务: code2session(校验身份)
微信平台服务-->>-用户上下文: sessionKey、 <br> appId、unionId等
用户上下文-->>-微信小程序: 登录结果 (JWT+result_code, 未签免密)
微信小程序->>微信小程序: 签署免密扣款协议
微信小程序->>+微信平台服务: 支付协议签署(微信内部协议)
微信平台服务-->>-用户上下文: 支付协议签署结果 <br> (contract+id)
微信小程序->>+用户上下文: (再次)登录smartrm(js_code)
用户上下文-->>-微信小程序: 登录结果 (JWT+result_code, 已签免密)
微信小程序->>+交易上下文: 打开货柜机柜门 <br> token
交易上下文-->>-微信小程序: 打开结果
之前这里的处理有一定问题,没有保证此段代码可靠性和事务性,一旦处理过程失败,用户可能就无法获得退款,用户体验差。应放到调度器里执行。
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的介入。