在DDD(领域驱动设计)项目中,应用服务层(Application Layer)和领域模型层(Domain Layer)的核心区别可以概括为:应用服务层是业务流程的“指挥家/编排者”,而领域模型层是业务核心的“发动机/规则制定者”。
应用服务层不包含任何核心业务逻辑,只负责协调和转发;领域模型层则承载了系统所有的业务规则、状态变化和逻辑校验。
以下是两者的详细对比、目录结构区别、适用场景以及完整的代码示例。
⚖️ 核心区别与职责对比
| 维度 | 应用服务层 (Application Layer) | 领域模型层 (Domain Layer) |
|---|---|---|
| 核心定位 | 业务流程的编排者和协调者 | 业务核心逻辑与规则的承载者 |
| 业务逻辑 | 严禁包含核心业务规则 | 包含所有核心业务规则、策略和完整性约束 |
| 主要职责 | 用例编排、事务控制、权限校验、DTO与领域对象转换、发布领域事件 | 表达业务概念、维护业务状态、执行领域行为(实体/值对象/聚合根)、跨聚合逻辑(领域服务) |
| 依赖关系 | 依赖领域层和基础设施层(调用仓储接口) | 零依赖(不依赖其他任何层,不依赖框架和技术细节) |
| 典型组件 | 应用服务 (Application Service)、命令/查询对象 (Command/Query)、DTO | 聚合根 (Aggregate Root)、实体 (Entity)、值对象 (Value Object)、领域服务 (Domain Service)、仓储接口 (Repository Interface) |
📂 目录结构区别
在标准的DDD四层架构中,这两层的代码目录通常如下划分(以Java/Maven项目为例):
com.example.project ├── application/ 【应用服务层】 │ ├── service/ # 应用服务接口与实现(如 OrderApplicationService) │ ├── dto/ # 数据传输对象(入参Command、出参DTO) │ └── assembler/ # 负责 DTO 与 领域对象 之间的转换(或叫 Converter) │ ├── domain/ 【领域模型层】 │ ├── model/ # 核心领域模型 │ │ ├── Order.java # 聚合根 (Aggregate Root) │ │ ├── OrderItem.java # 实体 (Entity) │ │ └── vo/ # 值对象 (Value Object,如 Address, Money) │ ├── service/ # 领域服务 (处理跨聚合的复杂业务逻辑) │ ├── event/ # 领域事件 (Domain Event) │ └── repository/ # 仓储接口 (Repository Interface,只定义接口,不含实现) │ └── infrastructure/ 【基础设施层】 └── repository/ # 仓储接口的具体实现 (如 OrderRepositoryImpl)🎯 场景选择与功能归属
在实际开发中,判断一段逻辑该写在哪一层,可以遵循以下原则:
- 适合放在【领域模型层】的场景:
- 单一对象的行为与规则:比如“订单金额计算”、“优惠券是否可用校验”、“订单状态从‘待支付’流转到‘已支付’”。这些逻辑应该封装在聚合根或实体的方法中(充血模型)。
- 跨多个聚合的业务规则:比如“转账业务”(涉及两个账户聚合的余额扣减与增加),或者“下单时校验用户信用额度并扣减库存”。这种不属于单一实体的逻辑,应放在领域服务中。
- 业务概念的封装:比如金额(Money)、收货地址(Address)等不可变对象及其自带的校验规则,应作为值对象。
- 适合放在【应用服务层】的场景:
- 业务流程的串联(用例):比如“用户下单”这个动作,需要依次执行:
参数转换 -> 调用领域服务校验 -> 调用聚合根创建订单 -> 调用仓储保存 -> 发布订单创建事件。应用服务只负责把这些步骤串起来。 - 技术层面的横切关注点:比如开启和提交数据库事务(
@Transactional)、当前登录用户的权限校验、记录操作日志等。 - 与外部世界的交互适配:接收前端传来的DTO,将其转换为领域层能理解的领域对象或命令(Command),或者将领域对象转换为返回给前端的DTO。
- 业务流程的串联(用例):比如“用户下单”这个动作,需要依次执行:
💻 完整代码示例参考
以一个电商系统的“创建订单”为例,对比传统贫血模型与DDD富血模型的写法。
1. 领域模型层(Domain Layer)实现
这里体现了业务规则的封装。Order聚合根负责保证自身数据的一致性,Money值对象负责金额的运算规则。
// 值对象:金额 (封装了金额的运算规则,不可变)publicclassMoney{privatefinalBigDecimalamount;privatefinalStringcurrency;publicMoneyadd(Moneyother){if(!this.currency.equals(other.currency)){thrownewIllegalArgumentException("货币单位不一致");}returnnewMoney(this.amount.add(other.amount),this.currency);}// ... 省略构造器与getter}// 聚合根:订单 (封装了订单创建的核心业务规则)publicclassOrder{privateOrderIdid;privateList<OrderItem>items;privateMoneytotalAmount;privateOrderStatusstatus;// 私有构造,通过工厂方法创建,确保创建出来的订单一定是合法的privateOrder(OrderIdid,List<OrderItem>items){this.id=id;this.items=items;this.status=OrderStatus.PENDING;// 业务规则:创建时自动计算总金额this.totalAmount=calculateTotalAmount(items);}// 工厂方法:创建订单publicstaticOrdercreate(List<OrderItem>items){if(items==null||items.isEmpty()){thrownewIllegalArgumentException("订单项不能为空");}// 业务规则:校验库存(这里假设订单项内部封装了库存校验逻辑)items.forEach(OrderItem::checkStock);returnnewOrder(newOrderId(UUID.randomUUID().toString()),items);}privateMoneycalculateTotalAmount(List<OrderItem>items){returnitems.stream().map(OrderItem::getSubtotal).reduce(newMoney(BigDecimal.ZERO,"CNY"),Money::add);}}// 领域服务接口(定义在领域层)publicinterfaceOrderRepository{voidsave(Orderorder);}2. 应用服务层(Application Layer)实现
这里体现了流程的编排。它不包含任何if (amount > 100)这样的业务判断,只是指挥领域对象去工作。
@ServicepublicclassOrderApplicationService{@AutowiredprivateOrderRepositoryorderRepository;// 注入领域层定义的仓储接口// 事务控制通常放在应用服务层@TransactionalpublicStringcreateOrder(CreateOrderCommandcommand){// 1. 参数转换与基础适配 (DTO -> 领域对象)List<OrderItem>items=command.getItems().stream().map(item->newOrderItem(item.getProductId(),item.getQuantity(),item.getPrice())).collect(Collectors.toList());// 2. 委托领域层执行核心业务逻辑 (调用聚合根的工厂方法)// 此时,库存校验、金额计算等业务规则都在 Order.create 内部自动完成Orderorder=Order.create(items);// 3. 持久化 (调用仓储接口,实际实现由基础设施层提供)orderRepository.save(order);// 4. 发布领域事件 (通知其他系统,如积分服务、物流服务)// domainEventPublisher.publish(new OrderCreatedEvent(order.getId()));returnorder.getId().getValue();}}总结:
当你需要修改业务规则(比如“满100减20”或“新用户才能用券”)时,你只需要去修改领域模型层的Order或Coupon实体;而当你需要调整业务流程(比如下单后增加一个短信通知步骤)时,你只需要修改应用服务层的编排逻辑。两者各司其职,保证了系统的可维护性和扩展性。