Fork me on GitHub

告别“大泥球”!理解 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 用户登录案例

用户登录时序图:

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的情况通常包括以下几种:

  1. 简单的CRUD操作:当应用层需要执行简单的创建、读取、更新或删除操作时,可以直接通过Repository与数据库进行交互,无需复杂的领域逻辑。
  2. 查询操作:当应用服务需要执行查询操作来获取数据,而这些数据不需要经过领域逻辑处理时,可以直接使用Repository来实现。
  3. 事务管理:在需要管理事务的情况下,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的介入。

posted @ 2025-12-01 15:57  公众号-JavaEdge  阅读(4)  评论(0)    收藏  举报