You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在不依赖基础设施的前提下实现DDD富模型并留存业务逻辑?

在DDD中实现富模型:处理外部依赖与保持领域纯粹的策略

在复杂系统中实现富模型的核心挑战,在于如何让领域逻辑内聚在实体/值对象中,同时合理处理对数据库或外部服务的依赖。以下是务实的落地策略和平衡方法:

一、依赖倒置+端口-适配器模式:隔离领域与外部依赖

核心思路是让领域模型依赖抽象端口,而非具体的外部实现,把依赖的具体实现移到基础设施层的适配器中,确保领域层不被外部细节污染。

比如订单实体需要检查库存才能添加订单项:

// 领域层定义抽象端口(不涉及任何外部实现细节)
public interface InventoryChecker {
    boolean hasSufficientStock(Sku sku, int quantity);
}

// 富模型订单实体:核心业务逻辑内聚
public class Order {
    private List<OrderLine> lines;
    private final InventoryChecker inventoryChecker;

    // 通过构造注入抽象依赖,避免硬编码外部服务
    public Order(InventoryChecker inventoryChecker) {
        this.inventoryChecker = inventoryChecker;
    }

    public void addOrderLine(Sku sku, int quantity) {
        // 核心业务规则留在领域内:添加订单项前必须校验库存
        if (!inventoryChecker.hasSufficientStock(sku, quantity)) {
            throw new InsufficientStockException(sku, quantity);
        }
        this.lines.add(new OrderLine(sku, quantity));
    }
}

基础设施层实现InventoryChecker接口,比如调用数据库查询库存或外部仓储服务,领域层完全不需要关心这些细节。

二、合理使用领域服务:承载跨实体/依赖外部的领域规则

不要把所有依赖外部的逻辑都丢到应用服务,而是用领域服务封装那些跨实体、或需要外部数据但仍属于领域规则的逻辑:

  • 领域服务属于领域层,只处理领域内的规则,不负责流程编排;
  • 应用服务负责调用领域服务、协调外部系统,处理非领域的流程逻辑。

比如计算订单折扣需要参考用户会员等级:

// 领域层抽象仓库
public interface MemberRepository {
    Member findById(MemberId memberId);
}

// 领域服务:封装折扣计算的领域规则
public class OrderDiscountService {
    private final MemberRepository memberRepository;

    public OrderDiscountService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public BigDecimal calculateDiscount(Order order) {
        Member member = memberRepository.findById(order.getMemberId());
        // 核心折扣规则:会员等级对应不同折扣率
        return switch (member.getLevel()) {
            case VIP -> new BigDecimal("0.8");
            case GOLD -> new BigDecimal("0.9");
            default -> BigDecimal.ONE;
        };
    }
}

这里折扣计算的规则属于领域逻辑,留在领域服务中;会员数据的获取依赖抽象仓库,具体实现由基础设施层完成。

三、值对象内聚逻辑:通过参数传递外部数据

对于不需要持有外部依赖的业务逻辑,优先封装到值对象中,外部数据通过方法参数传入,避免值对象直接依赖外部服务。

比如货币转换逻辑:

public class Money {
    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }

    // 转换逻辑内聚在值对象中,汇率作为参数传入
    public Money convertTo(Currency targetCurrency, BigDecimal exchangeRate) {
        BigDecimal convertedAmount = amount.multiply(exchangeRate);
        return new Money(convertedAmount.setScale(2, RoundingMode.HALF_UP), targetCurrency);
    }
}

汇率的获取由上层(领域服务或应用服务)负责,值对象只专注于货币转换的核心逻辑,保持纯粹性。

四、预加载聚合内数据:让实体可独立完成逻辑

对于聚合内部的关联数据,在加载聚合根时通过仓库预加载,让实体在内存中可以独立执行大部分业务逻辑,减少对外部依赖的实时调用。

比如加载订单时,同时预加载关联的会员等级信息:

// 领域层仓库接口
public interface OrderRepository {
    Order findByIdWithMember(OrderId orderId);
}

// 订单实体使用预加载的会员数据计算折扣
public class Order {
    private Member member;
    private List<OrderLine> lines;

    public BigDecimal getDiscountAmount() {
        // 直接使用预加载的会员数据,无需实时调用外部服务
        return switch (member.getLevel()) {
            case VIP -> getTotalAmount().multiply(new BigDecimal("0.2"));
            case GOLD -> getTotalAmount().multiply(new BigDecimal("0.1"));
            default -> BigDecimal.ZERO;
        };
    }

    private BigDecimal getTotalAmount() {
        return lines.stream()
                .map(line -> line.getPrice().multiply(new BigDecimal(line.getQuantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

注意预加载的数据必须在聚合边界内,避免加载过多无关数据导致性能问题。


平衡业务逻辑的关键:区分核心与支撑逻辑

要避免业务规则过度迁移到服务层,需明确划分:

  • 核心领域逻辑:比如订单状态流转规则、折扣计算、库存校验等,必须留在领域模型(实体、值对象、领域服务)中,这是DDD保持领域纯粹性的核心;
  • 支撑逻辑:比如调用支付接口、发送通知、日志记录等,属于应用层或基础设施层的职责,不应侵入领域层。

定期做代码审查和重构,把不小心漂移到服务层的核心逻辑迁回领域模型,保持领域的内聚性。

内容的提问来源于stack exchange,提问作者Julian Gr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 03:52:34