好的,这是一个非常经典且重要的问题。在领域驱动设计(DDD)中,明确区分**应用层(Application Layer)和领域层(Domain Layer)**是构建健壮、可维护软件的核心。
我们用一个常见的电商场景——“用户下单”——来具体说明哪些东西属于应用层,哪些属于领域层。
假设的四层结构:
- 表示层 (Presentation Layer): 用户的浏览器、手机App界面或API接口。
- 应用层 (Application Layer): 协调者,处理用例流程。
- 领域层 (Domain Layer): 核心,包含业务规则和状态。
- 基础设施层 (Infrastructure Layer): 提供技术支持,如数据库、消息队列等。
## 领域层 (Domain Layer):业务的核心和灵魂
领域层是软件的心脏,它只关心业务本身的规则和状态,完全不关心这些规则被谁、以何种方式使用。它体现了业务的本质。
在“用户下单”这个场景中,领域层包含以下元素:
- 领域实体 (Entities): 具有唯一标识符并且状态会随时间变化的对象。它们是业务的核心“名词”。
Order
(订单): 这是核心实体。它包含订单号、订单项列表、总金额、状态(如:待支付、已支付、已发货)、收货地址等。它还有自己的行为,比如cancel()
(取消订单)、markAsPaid()
(标记为已支付)。OrderItem
(订单项): 订单中的具体商品行。包含商品ID、数量、单价。Customer
(客户): 包含客户ID、姓名、客户等级等。它可能有isVip()
(判断是否为VIP) 这样的方法。Product
(商品): 包含商品ID、价格、库存数量。它有hasStock(quantity)
(检查库存是否充足) 这样的业务方法。
- 值对象 (Value Objects): 没有唯一标识符,通常用来度量或描述事物的对象。它们一旦创建就不可变。
Address
(地址): 由省、市、区、详细街道组成。两个地址对象,如果所有属性都相同,我们就可以认为它们是同一个地址。Money
(金额): 包含数值和货币单位(如199.99
,CNY
)。它可以封装货币计算的复杂性和精度问题。
- 领域服务 (Domain Services): 当某个业务操作不适合放在任何一个实体上时,就可以使用领域服务。它封装了跨多个实体的复杂业务逻辑。
PriceCalculatorService
(价格计算服务): 如果订单价格计算非常复杂,比如需要根据客户等级、促销活动、优惠券等多个因素来决定最终价格,这个逻辑就不适合放在Order
实体中,因为它依赖了Customer
和Promotion
等其他实体。这个服务会接收订单和客户信息,然后返回最终价格。
领域层的代码特点:
- 纯粹的业务逻辑,不包含任何技术实现代码(没有数据库SQL、没有HTTP请求)。
- 稳定,不随技术(UI、数据库)的改变而改变。
- 富含业务行为,而不仅仅是数据的get/set方法。例如,
Order
实体自己负责计算总价,而不是让应用层来计算。
## 应用层 (Application Layer):用例的协调者和指挥官
应用层是领域层的直接客户。它不包含任何业务规则,而是负责协调和编排领域对象来完成一个完整的用户场景(Use Case)。它像一个指挥官,告诉领域层的各个对象“现在该你做什么了”。
继续“用户下单”的场景,应用层包含以下元素:
- 应用服务 (Application Services): 它的方法通常直接对应一个用户用例。
OrderService
(订单服务): 注意,这里的服务和领域服务不同。这是一个应用服务。
我们来看看 OrderService
的一个典型方法 placeOrder(customerId, productQuantities, shippingAddress)
的内部流程:
- 开始事务: 与基础设施层交互,启动一个数据库事务。
- 数据获取: 调用基础设施层的仓储接口 (Repository),从数据库中加载必要的领域实体。
Customer customer = customerRepository.findById(customerId);
List<Product> products = productRepository.findByIds(productQuantities.keys());
- 验证和协调:
- 检查客户状态是否正常 (
customer.isActive()
)。 - 循环检查所有商品的库存 (
product.hasStock(quantity)
)。
- 检查客户状态是否正常 (
- 调用领域对象执行核心逻辑:
- 创建一个新的
Order
实体:Order newOrder = customer.createOrder(shippingAddress);
(这里假设Customer
实体有一个创建订单的工厂方法)。 - 将商品添加到订单中:
newOrder.addItems(products, productQuantities);
- 创建一个新的
- 持久化: 再次调用仓储接口,将新创建和已修改的实体保存回数据库。
orderRepository.save(newOrder);
productRepository.updateAll(products);
// 更新库存
- 触发副作用:
- 调用基础设施层的服务,发送一个“订单创建成功”的邮件或短信。
- 发布一个“订单已创建”的领域事件 (Domain Event) 到消息队列,供其他系统(如物流系统)消费。
- 提交事务: 结束数据库事务。
- 返回结果: 向表示层返回一个数据传输对象 (DTO),而不是直接返回领域实体。例如,返回
OrderConfirmationDTO
,其中包含订单号和总金额。
应用层的代码特点:
- 流程性强:代码读起来像一个操作步骤列表。
- “瘦”逻辑:本身不进行复杂的业务计算,而是调用领域对象的方法。
- 面向用例:每个公开方法都对应一个具体的用户功能。
- 处理横切关注点:如事务管理、安全认证、日志记录等。
## 总结与对比
特征 | 应用层 (Application Layer) | 领域层 (Domain Layer) |
职责 | 协调、编排、指挥 | 表达和执行业务规则 |
核心 | 用例 (Use Case) | 业务模型 (Business Model) |
关心什么 | 如何完成一个任务的流程 | 业务的状态和规则是什么 |
典型代码 | 1. 启动事务<br>2. 加载领域对象<br>3. 调用领域对象的方法<br>4. 持久化<br>5. 结束事务<br>6. 发送通知 | order.cancel() <br>product.hasStock() <br>priceCalculator.calculate() |
变化频率 | 较高,随用户需求和交互方式变化 | 较低,只在核心业务规则变化时才改变 |
依赖关系 | 依赖领域层和基础设施层接口 | 不依赖任何其他层,是系统的核心和独立部分 |
简单来说,领域层定义了“积木块”(是什么),而应用层负责“搭建积木”(怎么做)。这种分离使得业务核心(领域层)变得非常稳定和可测试,而应用流程(应用层)则可以灵活地适应外部变化。