第5章 领域服务 - 不属于实体的操作 (Domain Services - Operations That Don't Belong to Entities)
一、导读
5.1 本章学习目标
通过本章的学习,读者将能够:
理解领域服务(Domain Service)在 DDD 中的定义与职责
掌握领域服务与应用服务、基础设施服务的区别
学会识别何时应该使用领域服务
掌握设计领域服务的最佳实践
理解领域服务与实体、值对象的协作关系
能够在实际项目中正确设计和实现领域服务
避免领域服务的常见反模式
掌握处理跨聚合业务的领域服务模式
5.2 为什么本章如此重要
在领域建模过程中,我们会发现有些业务逻辑无法自然地归属于任何一个实体或值对象。这些操作可能涉及多个领域对象的协调,或者代表一个重要的业务概念,但本身没有状态需要维护。对于这类操作,DDD 引入了领域服务的概念。
领域服务是 DDD 战术设计中容易被误解和滥用的部分。一方面,如果将所有业务逻辑都放在实体中,会导致实体臃肿不堪;另一方面,如果过度使用领域服务,会导致贫血模型,失去面向对象的优势。正确把握领域服务的使用场景和边界,是构建高质量领域模型的关键。
在实际开发中,我们常常看到两种极端:一种是"服务泛滥",几乎所有业务逻辑都在服务层,实体只是数据容器;另一种是"服务恐惧",即使操作明显涉及多个聚合的协调,也强行塞进某个实体中。这两种做法都有问题。领域服务的存在有其合理性,关键是要在正确的场景下使用它。
领域服务还与分层架构密切相关。在典型的分层架构中,有应用服务、领域服务和基础设施服务三种不同类型的服务。混淆这些服务的职责会导致架构混乱,使得系统难以理解和维护。明确每种服务的定位和职责,是良好架构设计的基础。
此外,领域服务是实现业务规则的重要载体。许多复杂的业务规则,如风控检查、定价计算、库存分配等,往往需要通过领域服务来实现。掌握领域服务的设计技巧,对于准确表达业务逻辑至关重要。
5.3 核心问题引导
在学习本章之前,请思考以下问题:
什么是领域服务?它与普通的服务类有什么区别?
领域服务、应用服务和基础设施服务分别解决什么问题?它们的边界在哪里?
什么情况下应该使用领域服务,而不是将逻辑放在实体中?
领域服务可以有状态吗?它应该是无状态的吗?
领域服务可以调用仓储吗?如果可以,应该注意什么?
如何命名领域服务?如何组织领域服务的方法?
领域服务是否应该发布领域事件?
在微服务架构中,领域服务应该如何设计?
二、核心概念详解
5.1 领域服务的定义与本质
领域服务(Domain Service)是领域层中的一种服务对象,它封装了不属于任何单个实体或值对象的业务逻辑。领域服务代表了一个重要的业务概念或操作,但它本身没有需要维护的状态。
领域服务的本质在于它是一个"动词"而非"名词"。实体和值对象通常是名词,代表业务中的事物;而领域服务是动词,代表业务中的操作。当某个操作无法自然地归属于任何一个事物时,我们就引入领域服务来表达这个操作。
Eric Evans 在《领域驱动设计》中提出了判断是否应该使用领域服务的三个特征:
首先,操作涉及的概念是领域模型的一部分,但不是任何一个实体或值对象的自然职责。例如,转账操作涉及两个账户,但不属于任何一个账户。
其次,操作在领域中是有意义的,它代表了业务专家关注的一个重要概念。例如,"资金划转"、"风险评估"、"价格计算"等都是业务中的重要概念。
第三,操作是无状态的。领域服务本身不应该维护状态,它的行为完全由输入参数决定。相同的输入应该产生相同的输出。
需要注意的是,领域服务是领域层的组成部分,它应该只包含业务逻辑,不涉及技术细节。领域服务的接口和实现都应该使用业务语言,体现通用语言。
5.2 领域服务的特征
领域服务具有以下几个核心特征:
无状态性
领域服务本身不应该维护状态。它的行为完全由输入参数和依赖的领域对象决定。每次调用领域服务时,都应该提供所有必要的信息。
无状态性使得领域服务易于测试和推理。由于没有内部状态,领域服务的行为是可预测的,不会出现因状态变化导致的意外行为。
需要注意的是,领域服务可以依赖有状态的对象(如实体),但领域服务本身不维护状态。它可以读取实体的状态,执行操作,但不会在自己的字段中保存状态。
业务语义
领域服务的方法名和参数应该使用业务术语,体现通用语言。例如,transferFunds()、calculatePrice()、assessRisk() 等名称直接表达了业务意图。
业务语义使得领域服务成为业务知识的载体。阅读领域服务的代码,应该能够理解业务规则和流程。
协调性
领域服务通常用于协调多个领域对象完成一个业务操作。它编排实体和值对象的工作,确保业务规则得到正确执行。
例如,订单取消服务可能需要协调订单实体、库存实体、支付实体等多个对象,执行一系列的操作和验证。
可复用性
领域服务封装的业务逻辑应该是可复用的。如果同一段业务逻辑在多处出现,考虑将其提取为领域服务。
可复用性是领域服务存在的重要理由之一。通过将公共业务逻辑集中到领域服务中,可以避免代码重复,提高一致性。
5.3 领域服务与其他服务的区别
在分层架构中,有三种不同类型的服务,它们的职责和定位各不相同:
应用服务(Application Service)
应用服务位于应用层,负责协调领域对象完成用例。它的主要职责包括:
- 接收用户请求,解析参数
- 开启和提交事务
- 调用领域服务或实体执行业务逻辑
- 发布领域事件
- 返回结果
应用服务不包含业务规则,它只是业务逻辑的编排者。应用服务的方法通常对应一个用户故事或用例。
领域服务(Domain Service)
领域服务位于领域层,包含业务规则和逻辑。它的主要职责包括:
- 封装跨实体的业务逻辑
- 执行复杂的业务计算
- 协调多个聚合的操作
- 表达重要的业务概念
领域服务包含真正的业务规则,是领域知识的核心载体。
基础设施服务(Infrastructure Service)
基础设施服务位于基础设施层,提供技术能力。它的主要职责包括:
- 数据库访问
- 消息发送
- 文件存储
- 外部 API 调用
基础设施服务不包含业务逻辑,它只提供技术支持。基础设施服务通常通过接口被领域层使用,以实现依赖倒置。
三者的关系可以用一个简单的例子说明:
当用户点击"转账"按钮时,应用服务接收请求,验证权限,开启事务;然后调用领域服务执行转账逻辑(检查余额、扣款、存款);领域服务通过仓储接口获取账户实体,执行操作;基础设施服务负责实际的数据库更新。
5.4 何时使用领域服务
判断是否应该使用领域服务,可以考虑以下场景:
跨聚合操作
当操作涉及多个聚合,且无法将责任归于任何一个聚合时,使用领域服务。例如,转账涉及两个账户,预订涉及航班和乘客。
在这种情况下,强行将逻辑放在某个聚合中会导致该聚合承担不属于它的责任,破坏聚合的内聚性。
业务规则复杂
当业务规则涉及多个条件判断、复杂的计算或外部依赖时,使用领域服务可以将这些复杂性从实体中剥离出来。
例如,贷款审批涉及信用评分、收入验证、负债比率等多个因素,将这些逻辑放在领域服务中更清晰。
操作不代表实体的自然行为
当操作不是实体的自然行为时,使用领域服务。例如,"发送通知"不是订单的自然行为,而是独立的操作。
判断是否是"自然行为"的一个方法是问:业务专家是否会说"订单发送通知"?如果不会,可能就不应该将这个方法放在订单实体中。
需要复用业务逻辑
当同一段业务逻辑在多处使用时,提取为领域服务可以避免重复。
例如,价格计算逻辑可能在下单、改价、退款等多个场景中使用,提取为领域服务可以保证一致性。
操作是重要的业务概念
当操作本身是一个重要的业务概念时,即使它可以放在实体中,也可以考虑使用领域服务来突出其重要性。
例如,"风险评估"可能只涉及一个客户实体,但由于它是金融领域的核心概念,使用领域服务可以更好地表达其重要性。
5.5 何时不使用领域服务
以下情况不应该使用领域服务:
逻辑属于单个实体
如果操作只涉及一个实体,且不依赖其他对象,应该将逻辑放在实体中。例如,订单取消、用户改名等操作应该是对应实体的方法。
将这类逻辑放在领域服务中会导致贫血模型,实体失去行为,变成纯粹的数据容器。
简单的 CRUD 操作
如果只是简单的数据读写,不需要领域服务。仓储已经提供了持久化能力,应用服务可以直接调用仓储。
领域服务应该包含业务逻辑,而不是简单的数据操作。
技术实现细节
如果操作主要是技术实现,如数据转换、格式化处理等,应该放在基础设施层,而不是领域层。
领域服务应该关注业务,而不是技术。
可以重构为值对象行为
如果操作可以通过引入值对象来解决,优先考虑值对象。例如,金额计算可以通过 Money 值对象实现,而不是金额计算服务。
值对象比领域服务更轻量,更符合面向对象的原则。
5.6 领域服务的设计原则
设计良好的领域服务应该遵循以下原则:
保持精简
领域服务应该只做它应该做的事。不要将不属于领域服务的逻辑放入其中。如果一个领域服务变得过于庞大,考虑拆分或重构。
使用业务语言
领域服务的命名和方法签名应该使用通用语言。阅读领域服务的代码应该能够理解业务意图。
依赖抽象
领域服务如果需要访问数据,应该依赖仓储接口,而不是具体实现。这样可以保持领域层的独立性。
无副作用
领域服务的行为应该尽可能无副作用。它应该通过修改传入的领域对象来产生效果,而不是自己维护状态。
可测试性
领域服务应该易于单元测试。通过依赖注入,可以替换掉外部依赖,专注于业务逻辑的测试。
明确输入输出
领域服务的方法应该有明确的输入参数和返回值。避免使用隐式的全局状态或上下文。
三、程序员的视角
5.1 在实际项目中应用领域服务
作为程序员,在日常工作中设计和实现领域服务时,应该注意以下几个方面:
首先,在创建新的服务类之前,先判断它应该放在哪一层。问自己:这个服务包含业务逻辑吗?它是否属于领域层?如果答案是否定的,可能应该放在应用层或基础设施层。
其次,设计领域服务的接口时,使用业务术语命名方法和参数。让接口的使用者能够通过方法名理解业务意图。
第三,保持领域服务的无状态性。不要在领域服务中保存状态,每次调用都应该提供所有必要的信息。如果需要记住什么,通过参数传递。
第四,领域服务可以依赖仓储来获取领域对象,但不要直接在领域服务中进行持久化操作。持久化是应用层的职责,领域服务只负责业务逻辑。
第五,当领域服务修改了领域对象的状态,应该在应用层触发持久化。领域服务本身不负责保存,它只负责修改内存中的对象。
第六,编写全面的单元测试覆盖领域服务。由于领域服务无状态且依赖抽象,它们通常很容易测试。使用 mock 对象替换外部依赖。
5.2 常见陷阱与误区
在使用领域服务时,常见的陷阱包括:
陷阱一:服务泛滥。将所有业务逻辑都放在领域服务中,实体只有 getter/setter。这导致贫血模型,失去了面向对象的优势。
陷阱二:服务过大。一个领域服务包含了太多不相关的操作,变得难以理解和维护。应该按业务概念拆分领域服务。
陷阱三:状态泄漏。在领域服务中保存状态,导致服务变得有状态,行为不可预测。领域服务应该是无状态的。
陷阱四:层次混淆。在领域服务中调用基础设施服务的具体实现,或直接访问数据库。领域服务应该只依赖抽象。
陷阱五:命名不当。使用技术化的命名如 Processor、Manager、Helper,而不是业务术语。领域服务的命名应该反映业务概念。
陷阱六:过度设计。为简单的操作创建领域服务,增加了不必要的复杂性。简单的逻辑应该放在实体中。
陷阱七:循环依赖。领域服务 A 调用领域服务 B,B 又调用 A,形成循环依赖。应该重新设计服务边界。
5.3 与其他模式的协作
领域服务与其他 DDD 模式紧密协作:
与实体协作:领域服务可以读取和修改实体的状态,执行实体无法独立完成的操作。
与值对象协作:领域服务可以使用值对象作为参数和返回值,值对象提供不可变的数据结构。
与仓储协作:领域服务通过仓储接口获取和保存领域对象,但不直接进行持久化。
与领域事件协作:领域服务可以在执行业务操作后触发领域事件,通知其他组件。
与应用服务协作:应用服务调用领域服务执行业务逻辑,领域服务返回结果给应用服务。
四、代码实战
5.1 Java 示例:转账领域服务
/**
* 转账领域服务
*
* 转账操作涉及两个账户的协调,不属于任何一个账户的自然职责,
* 因此使用领域服务来表达这个业务概念。
*/
public class FundsTransferService {
private final AccountRepository accountRepository;
private final TransferLimitPolicy transferLimitPolicy;
private final DomainEventPublisher eventPublisher;
public FundsTransferService(AccountRepository accountRepository,
TransferLimitPolicy transferLimitPolicy,
DomainEventPublisher eventPublisher) {
this.accountRepository = accountRepository;
this.transferLimitPolicy = transferLimitPolicy;
this.eventPublisher = eventPublisher;
}
/**
* 执行转账
*
* 这是一个典型的跨聚合操作:
* 1. 从付款账户扣款
* 2. 向收款账户存款
* 3. 记录交易日志
* 4. 发布领域事件
*/
public TransferResult transfer(TransferCommand command) {
// 验证命令
validateCommand(command);
// 获取账户
Account payerAccount = accountRepository.findById(command.getPayerAccountId());
if (payerAccount == null) {
return TransferResult.failure("Payer account not found");
}
Account payeeAccount = accountRepository.findById(command.getPayeeAccountId());
if (payeeAccount == null) {
return TransferResult.failure("Payee account not found");
}
// 检查是否是同一账户
if (payerAccount.getId().equals(payeeAccount.getId())) {
return TransferResult.failure("Cannot transfer to the same account");
}
// 限额检查
if (!transferLimitPolicy.isWithinLimit(payerAccount, command.getAmount())) {
return TransferResult.failure("Transfer amount exceeds limit");
}
// 执行转账
try {
Transaction transaction = Transaction.initiate(
payerAccount.getId(),
payeeAccount.getId(),
command.getAmount(),
command.getDescription()
);
// 扣款和存款
payerAccount.debit(transaction);
payeeAccount.credit(transaction);
// 保存账户(在应用层实际持久化)
accountRepository.save(payerAccount);
accountRepository.save(payeeAccount);
// 发布领域事件
eventPublisher.publish(new FundsTransferredEvent(
transaction.getId(),
payerAccount.getId(),
payeeAccount.getId(),
command.getAmount(),
LocalDateTime.now()
));
return TransferResult.success(transaction.getId());
} catch (InsufficientFundsException e) {
return TransferResult.failure("Insufficient funds");
} catch (Exception e) {
// 记录错误日志
return TransferResult.failure("Transfer failed: " + e.getMessage());
}
}
private void validateCommand(TransferCommand command) {
if (command.getAmount() == null || !command.getAmount().isPositive()) {
throw new IllegalArgumentException("Transfer amount must be positive");
}
if (command.getPayerAccountId() == null) {
throw new IllegalArgumentException("Payer account is required");
}
if (command.getPayeeAccountId() == null) {
throw new IllegalArgumentException("Payee account is required");
}
}
}
/**
* 转账命令对象
*/
public class TransferCommand {
private final AccountId payerAccountId;
private final AccountId payeeAccountId;
private final Money amount;
private final String description;
public TransferCommand(AccountId payerAccountId, AccountId payeeAccountId,
Money amount, String description) {
this.payerAccountId = payerAccountId;
this.payeeAccountId = payeeAccountId;
this.amount = amount;
this.description = description;
}
// Getters
public AccountId getPayerAccountId() { return payerAccountId; }
public AccountId getPayeeAccountId() { return payeeAccountId; }
public Money getAmount() { return amount; }
public String getDescription() { return description; }
}
/**
* 转账结果
*/
public class TransferResult {
private final boolean success;
private final String errorMessage;
private final String transactionId;
private TransferResult(boolean success, String errorMessage, String transactionId) {
this.success = success;
this.errorMessage = errorMessage;
this.transactionId = transactionId;
}
public static TransferResult success(String transactionId) {
return new TransferResult(true, null, transactionId);
}
public static TransferResult failure(String errorMessage) {
return new TransferResult(false, errorMessage, null);
}
public boolean isSuccess() { return success; }
public String getErrorMessage() { return errorMessage; }
public String getTransactionId() { return transactionId; }
}5.2 Python 示例:定价领域服务
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import List, Optional
class CustomerTier(Enum):
"""客户等级"""
REGULAR = "regular"
SILVER = "silver"
GOLD = "gold"
PLATINUM = "platinum"
@dataclass(frozen=True)
class PricingRule:
"""值对象:定价规则"""
min_quantity: int
discount_percentage: float
def __post_init__(self):
if self.min_quantity < 0:
raise ValueError("Min quantity cannot be negative")
if not (0 <= self.discount_percentage <= 100):
raise ValueError("Discount percentage must be between 0 and 100")
@dataclass(frozen=True)
class PriceQuote:
"""值对象:报价"""
unit_price: float
quantity: int
subtotal: float
discount: float
final_total: float
def __post_init__(self):
if self.unit_price < 0 or self.quantity < 0:
raise ValueError("Price and quantity must be non-negative")
class PricingService:
"""定价领域服务
负责根据多种因素计算商品价格,包括:
- 基础价格
- 数量折扣
- 客户等级折扣
- 促销活动
"""
def __init__(self, pricing_rules: List[PricingRule]):
self._pricing_rules = sorted(pricing_rules, key=lambda r: r.min_quantity, reverse=True)
def calculate_price(self, base_price: float, quantity: int,
customer_tier: CustomerTier,
promotion_code: Optional[str] = None) -> PriceQuote:
"""计算最终价格
Args:
base_price: 商品基础单价
quantity: 购买数量
customer_tier: 客户等级
promotion_code: 促销码(可选)
Returns:
PriceQuote: 报价详情
"""
if base_price < 0:
raise ValueError("Base price cannot be negative")
if quantity <= 0:
raise ValueError("Quantity must be positive")
# 计算小计
subtotal = base_price * quantity
# 计算数量折扣
quantity_discount = self._calculate_quantity_discount(subtotal, quantity)
# 计算客户等级折扣
tier_discount = self._calculate_tier_discount(subtotal - quantity_discount, customer_tier)
# 计算促销折扣
promotion_discount = self._calculate_promotion_discount(
subtotal - quantity_discount - tier_discount,
promotion_code
)
# 计算最终价格
total_discount = quantity_discount + tier_discount + promotion_discount
final_total = max(0, subtotal - total_discount)
return PriceQuote(
unit_price=base_price,
quantity=quantity,
subtotal=subtotal,
discount=total_discount,
final_total=round(final_total, 2)
)
def _calculate_quantity_discount(self, subtotal: float, quantity: int) -> float:
"""计算数量折扣"""
for rule in self._pricing_rules:
if quantity >= rule.min_quantity:
return subtotal * (rule.discount_percentage / 100)
return 0
def _calculate_tier_discount(self, amount: float, tier: CustomerTier) -> float:
"""计算客户等级折扣"""
tier_discounts = {
CustomerTier.REGULAR: 0,
CustomerTier.SILVER: 5,
CustomerTier.GOLD: 10,
CustomerTier.PLATINUM: 15
}
discount_percentage = tier_discounts.get(tier, 0)
return amount * (discount_percentage / 100)
def _calculate_promotion_discount(self, amount: float, promotion_code: Optional[str]) -> float:
"""计算促销折扣"""
if not promotion_code:
return 0
# 简化的促销逻辑
promotions = {
"SAVE10": 10,
"SAVE20": 20,
"HALF": 50
}
discount_percentage = promotions.get(promotion_code.upper(), 0)
return amount * (discount_percentage / 100)
# 使用示例
def pricing_example():
"""演示定价服务的使用"""
# 定义定价规则
rules = [
PricingRule(min_quantity=100, discount_percentage=15),
PricingRule(min_quantity=50, discount_percentage=10),
PricingRule(min_quantity=10, discount_percentage=5),
]
# 创建定价服务
pricing_service = PricingService(rules)
# 计算价格
quote = pricing_service.calculate_price(
base_price=29.99,
quantity=55,
customer_tier=CustomerTier.GOLD,
promotion_code="SAVE10"
)
print(f"Unit price: ${quote.unit_price}")
print(f"Quantity: {quote.quantity}")
print(f"Subtotal: ${quote.subtotal:.2f}")
print(f"Discount: ${quote.discount:.2f}")
print(f"Final total: ${quote.final_total:.2f}")5.3 领域服务与应用服务的协作
/**
* 应用服务:订单应用服务
*
* 负责协调领域对象完成"创建订单"用例
*/
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final PricingService pricingService; // 领域服务
private final InventoryService inventoryService; // 领域服务
private final DomainEventPublisher eventPublisher;
public OrderApplicationService(OrderRepository orderRepository,
ProductRepository productRepository,
PricingService pricingService,
InventoryService inventoryService,
DomainEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.pricingService = pricingService;
this.inventoryService = inventoryService;
this.eventPublisher = eventPublisher;
}
/**
* 创建订单
*
* 应用服务负责:
* 1. 接收请求
* 2. 开启事务
* 3. 调用领域服务执行业务逻辑
* 4. 持久化
* 5. 发布事件
* 6. 返回结果
*/
@Transactional
public CreateOrderResult createOrder(CreateOrderRequest request) {
try {
// 获取客户
Customer customer = customerRepository.findById(request.getCustomerId());
if (customer == null) {
return CreateOrderResult.failure("Customer not found");
}
// 获取商品信息
List<Product> products = productRepository.findByIds(request.getProductIds());
// 调用定价领域服务计算价格
PricingContext pricingContext = new PricingContext(customer.getTier(), request.getPromotionCode());
Money totalAmount = pricingService.calculateTotal(products, request.getQuantities(), pricingContext);
// 调用库存领域服务检查并预留库存
inventoryService.reserveStock(products, request.getQuantities());
// 创建订单实体
Order order = orderFactory.createOrder(customer, products, request.getQuantities(), totalAmount);
// 持久化
orderRepository.save(order);
// 发布领域事件
eventPublisher.publish(new OrderCreatedEvent(order.getId(), customer.getId(), totalAmount));
return CreateOrderResult.success(order.getId());
} catch (Exception e) {
// 事务回滚
return CreateOrderResult.failure("Order creation failed: " + e.getMessage());
}
}
}五、深入思考
5.1 历史背景
服务的概念在软件工程中有着悠久的历史。在面向过程编程时代,函数和服务是主要的代码组织方式。随着面向对象编程的兴起,重点转向了对象和类。然而,人们逐渐发现,并非所有的业务逻辑都能自然地归属于对象。
Eric Evans 在《领域驱动设计》中正式引入了领域服务的概念,将其作为实体和值对象的补充。他指出,有些业务概念本身就是操作,而不是事物。对于这些概念,领域服务是合适的表达方式。
随后,分层架构的普及进一步明确了不同类型服务的职责。应用服务、领域服务和基础设施服务的区分,成为了现代软件架构的标准模式。
5.2 前沿方向
领域服务的实践正在向以下几个方向发展:
首先是函数式编程的影响。在一些函数式风格的 DDD 实现中,领域服务被实现为纯函数,输入领域对象,返回修改后的领域对象。这种方式强调了领域服务的无状态性和可预测性。
其次是 CQRS 模式的应用。在 CQRS 中,查询和命令被分离。领域服务主要处理命令(写操作),而查询通过专门的读模型处理。这种分离简化了领域服务的职责。
第三是与事件驱动架构的结合。领域服务在执行操作后可以发布领域事件,其他组件通过订阅事件来响应。这种松耦合的方式提高了系统的可扩展性。
5.3 延伸阅读
Eric Evans, 《领域驱动设计》第5章 - 原始论述
Vaughn Vernon, 《实现领域驱动设计》第7章 - 实用指南
Martin Fowler, 《Service Layer》- Patterns of Enterprise Application Architecture
Udi Dahan, 《Don't Create Aggregate Roots》- 关于领域服务的文章
六、实验笔记
实验一:识别和重构领域服务
实验目的:通过分析现有代码,练习识别应该使用领域服务的场景。
实验步骤:
选择一个现有项目,找出其中的服务类。
对每个服务类,判断它属于哪一层:应用层、领域层还是基础设施层。
找出那些包含业务逻辑但被错误放置的服务(如应该在领域层却在应用层)。
找出那些应该提取为领域服务的逻辑(如散落在多个地方的重复业务逻辑)。
设计重构方案,将业务逻辑移动到正确的位置。
预期收获:培养识别领域服务的能力,掌握服务分层的原则。
实验二:设计一个风控领域服务
实验目的:练习设计复杂的领域服务。
实验步骤:
设计一个贷款审批的风控领域服务。
风控需要考虑的因素包括:信用评分、收入水平、负债比率、历史还款记录等。
设计服务的输入(贷款申请)和输出(审批结果)。
实现风控规则的检查逻辑。
编写单元测试覆盖各种场景。
预期收获:掌握复杂领域服务的设计技巧,学习如何处理多条件业务规则。
七、本章小结
本章详细介绍了 DDD 中的领域服务模式。我们首先定义了领域服务的本质:封装不属于任何单个实体或值对象的业务逻辑。然后,我们探讨了领域服务的核心特征:无状态性、业务语义、协调性和可复用性。
领域服务与应用服务、基础设施服务的区别是本章的重点内容。我们明确了三种服务的职责边界:应用服务负责编排,领域服务负责业务逻辑,基础设施服务负责技术实现。
何时使用领域服务是另一个关键主题。我们讨论了跨聚合操作、复杂业务规则、非自然行为等适用场景,以及逻辑属于单个实体、简单 CRUD 等不适用场景。
通过代码实战,我们展示了如何实现转账领域服务、定价领域服务,以及领域服务与应用服务的协作。这些示例体现了领域服务的最佳实践:无状态、业务语义、依赖抽象。
最后,我们回顾了领域服务的历史背景,展望了前沿方向,并提供了两个实验任务帮助读者实践所学内容。
领域服务是领域模型的重要组成部分。正确使用领域服务可以使业务逻辑更加清晰、可复用、易于维护。希望读者能够在实际项目中应用本章的知识,设计出优雅的领域服务。
八、习题
概念理解题:用自己的话解释什么是领域服务,它的核心特征是什么?
辨析题:以下操作哪些应该放在实体中,哪些应该使用领域服务?说明理由。
- 用户更改密码
- 批量导入商品
- 计算订单总价
- 发送订单确认邮件
- 评估贷款风险
- 更新商品库存
设计题:为一个电商系统设计一个"优惠券计算"领域服务。优惠券可能有满减、折扣、包邮等类型,还可能有限制条件(如最低消费、适用商品范围等)。设计服务的接口和核心逻辑。
代码分析题:以下代码有什么问题?如何改进?
public class UserService {
public void saveUser(User user) {
// 直接操作数据库
jdbcTemplate.update("INSERT INTO users ...");
}
public User getUser(Long id) {
return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", id);
}
}思考题:为什么领域服务应该是无状态的?有状态的领域服务会带来什么问题?
实践题:在你当前参与的项目中,找出一个领域服务,分析它的设计是否合理。如果有问题,提出改进方案。
论述题:领域服务和工具类(Utility Class)有什么区别?为什么不应该在领域层使用工具类?
设计题:设计一个"库存分配"领域服务,用于在多个仓库之间分配订单商品。考虑的因素包括:各仓库库存量、配送距离、仓库优先级等。写出核心代码。
开放题:在微服务架构中,领域服务是否应该跨服务调用?如果不能,应该如何处理跨服务的业务逻辑?
综合题:分析一个开源项目的服务分层。评价其领域服务的设计,指出优点和不足,并提出改进建议。