5领域驱动设计

领域服务

不属于实体的操作

领域服务应用服务无状态

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 示例:转账领域服务

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 示例:定价领域服务

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 领域服务与应用服务的协作

java
/**
 * 应用服务:订单应用服务
 * 
 * 负责协调领域对象完成"创建订单"用例
 */
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 等不适用场景。

通过代码实战,我们展示了如何实现转账领域服务、定价领域服务,以及领域服务与应用服务的协作。这些示例体现了领域服务的最佳实践:无状态、业务语义、依赖抽象。

最后,我们回顾了领域服务的历史背景,展望了前沿方向,并提供了两个实验任务帮助读者实践所学内容。

领域服务是领域模型的重要组成部分。正确使用领域服务可以使业务逻辑更加清晰、可复用、易于维护。希望读者能够在实际项目中应用本章的知识,设计出优雅的领域服务。


八、习题

概念理解题:用自己的话解释什么是领域服务,它的核心特征是什么?

辨析题:以下操作哪些应该放在实体中,哪些应该使用领域服务?说明理由。

- 用户更改密码

- 批量导入商品

- 计算订单总价

- 发送订单确认邮件

- 评估贷款风险

- 更新商品库存

设计题:为一个电商系统设计一个"优惠券计算"领域服务。优惠券可能有满减、折扣、包邮等类型,还可能有限制条件(如最低消费、适用商品范围等)。设计服务的接口和核心逻辑。

代码分析题:以下代码有什么问题?如何改进?

java
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)有什么区别?为什么不应该在领域层使用工具类?

设计题:设计一个"库存分配"领域服务,用于在多个仓库之间分配订单商品。考虑的因素包括:各仓库库存量、配送距离、仓库优先级等。写出核心代码。

开放题:在微服务架构中,领域服务是否应该跨服务调用?如果不能,应该如何处理跨服务的业务逻辑?

综合题:分析一个开源项目的服务分层。评价其领域服务的设计,指出优点和不足,并提出改进建议。

5/12领域服务
0%