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

领域驱动设计:调用外部服务丰富实体属性的可行方案探讨

DDD视角下的Product FinalPrice计算方案优化

问题背景

我为学习需求设计了一个支持CRUD的product-service,其中Product实体包含string ID、string Name、decimal ListPrice、decimal FinalPrice属性:

  • ID、Name、ListPrice存储在product-service的数据库中
  • FinalPrice需要从外部服务price-card-service的GET /price-card-by-id/{product-id}接口获取
  • 创建/更新操作不允许修改FinalPrice,仅在查询时补充该属性

当前实现是在GetProductByIdRequestHandler中处理FinalPrice的获取与设置,代码如下:

public class GetProductByIdRequestHandler : IAppRequestHandler<GetProductByIdRequest, GetProductByIdResponse>
{
    private readonly IProductRepository _productRepository;
    private readonly PriceCardServiceClient _priceCardServiceClient;

    public GetProductByIdRequestHandler(IProductRepository productRepository, PriceCardServiceClient priceCardServiceClient)
    {
        _productRepository = productRepository;
        _priceCardServiceClient = priceCardServiceClient;
    }

    public async Task<OneOf<GetProductByIdResponse, IError>> Handle(GetProductByIdRequest request, CancellationToken cancellationToken)
    {
        var product = await _productRepository.GetById(request.ProductId);
        
        if (product == null)
        {
            return new NotFoundError
            {
                Message = $"Could not find product {request.ProductId}"
            };
        }

        var priceCardList = await _priceCardServiceClient.ActiveAsync(product.Id, cancellationToken);

        if (!priceCardList.Items.Any())
        {
            product.FinalPrice = product.Price;
            return new GetProductByIdResponse(product);
        }
        var priceCard = priceCardList.Items.First();
        if (priceCard.NewPrice < 0)
        {
            return new PriceCardNewPriceLessThanZeroError
            {
                Message = $"Price {priceCard.NewPrice} for PriceCard {priceCard.Id} for Product {product.Id} must be greater or equal to 0"
            };
        }
        product.FinalPrice = ProductPrice.From(System.Convert.ToDecimal(priceCard.NewPrice));

        return new GetProductByIdResponse(product);
    }
}

想了解DDD是否推荐其他处理方式,比如将PriceCardServiceClient注入Product实体并创建只读FinalPrice属性来处理逻辑,有哪些可行方案?

可行方案

方案1:领域服务封装外部依赖逻辑

DDD中不建议直接将外部服务客户端注入实体(会破坏实体的独立性和可测试性),更推荐用领域服务封装跨边界的逻辑:

  1. 定义领域服务接口IProductPricingService,负责计算FinalPrice:
public interface IProductPricingService
{
    Task<decimal> CalculateFinalPrice(Product product, CancellationToken cancellationToken);
}
  1. 实现该接口,注入PriceCardServiceClient处理外部调用:
public class ProductPricingService : IProductPricingService
{
    private readonly PriceCardServiceClient _priceCardServiceClient;

    public ProductPricingService(PriceCardServiceClient priceCardServiceClient)
    {
        _priceCardServiceClient = priceCardServiceClient;
    }

    public async Task<decimal> CalculateFinalPrice(Product product, CancellationToken cancellationToken)
    {
        var priceCardList = await _priceCardServiceClient.ActiveAsync(product.Id, cancellationToken);
        
        if (!priceCardList.Items.Any())
        {
            return product.ListPrice;
        }
        
        var priceCard = priceCardList.Items.First();
        if (priceCard.NewPrice < 0)
        {
            throw new InvalidPriceException($"Price {priceCard.NewPrice} for PriceCard {priceCard.Id} must be >= 0");
        }
        
        return ProductPrice.From(Convert.ToDecimal(priceCard.NewPrice)).Value;
    }
}
  1. 在请求处理器中注入领域服务,调用计算逻辑:
public class GetProductByIdRequestHandler : IAppRequestHandler<GetProductByIdRequest, GetProductByIdResponse>
{
    private readonly IProductRepository _productRepository;
    private readonly IProductPricingService _productPricingService;

    public GetProductByIdRequestHandler(IProductRepository productRepository, IProductPricingService productPricingService)
    {
        _productRepository = productRepository;
        _productPricingService = productPricingService;
    }

    public async Task<OneOf<GetProductByIdResponse, IError>> Handle(GetProductByIdRequest request, CancellationToken cancellationToken)
    {
        var product = await _productRepository.GetById(request.ProductId);
        
        if (product == null)
        {
            return new NotFoundError { Message = $"Could not find product {request.ProductId}" };
        }

        try
        {
            var finalPrice = await _productPricingService.CalculateFinalPrice(product, cancellationToken);
            product.SetFinalPrice(finalPrice); // 实体内部提供受控的设置方法
            return new GetProductByIdResponse(product);
        }
        catch (InvalidPriceException ex)
        {
            return new PriceCardNewPriceLessThanZeroError { Message = ex.Message };
        }
    }
}
  1. 调整Product实体,将FinalPrice设为私有字段,仅通过SetFinalPrice方法设置,保证只读性(创建/更新时无法修改):
public class Product
{
    public string Id { get; private set; }
    public string Name { get; private set; }
    public decimal ListPrice { get; private set; }
    public decimal FinalPrice { get; private set; }

    // 构造函数、创建/更新方法...

    // 仅允许外部通过此方法设置FinalPrice,保证查询时才赋值
    public void SetFinalPrice(decimal finalPrice)
    {
        FinalPrice = finalPrice;
    }
}

方案2:查询模型分离(CQRS思路)

如果系统适合引入CQRS,可以将读模型和写模型分离:

  • 写模型:Product实体仅包含ID、Name、ListPrice,负责业务规则和持久化
  • 读模型:创建ProductWithFinalPrice DTO,专门用于查询场景,在查询时直接聚合外部服务数据

实现步骤:

  1. 写模型Product去掉FinalPrice属性:
public class Product
{
    public string Id { get; private set; }
    public string Name { get; private set; }
    public decimal ListPrice { get; private set; }

    // 构造函数、业务方法...
}
  1. 定义读模型DTO:
public class ProductWithFinalPrice
{
    public string Id { get; set; }
    public string Name { get; set; }
    public decimal ListPrice { get; set; }
    public decimal FinalPrice { get; set; }
}
  1. 查询处理器直接聚合数据:
public class GetProductByIdRequestHandler : IAppRequestHandler<GetProductByIdRequest, ProductWithFinalPrice>
{
    private readonly IProductRepository _productRepository;
    private readonly PriceCardServiceClient _priceCardServiceClient;

    public GetProductByIdRequestHandler(IProductRepository productRepository, PriceCardServiceClient priceCardServiceClient)
    {
        _productRepository = productRepository;
        _priceCardServiceClient = priceCardServiceClient;
    }

    public async Task<OneOf<ProductWithFinalPrice, IError>> Handle(GetProductByIdRequest request, CancellationToken cancellationToken)
    {
        var product = await _productRepository.GetById(request.ProductId);
        
        if (product == null)
        {
            return new NotFoundError { Message = $"Could not find product {request.ProductId}" };
        }

        var priceCardList = await _priceCardServiceClient.ActiveAsync(product.Id, cancellationToken);
        decimal finalPrice = product.ListPrice;

        if (priceCardList.Items.Any())
        {
            var priceCard = priceCardList.Items.First();
            if (priceCard.NewPrice < 0)
            {
                return new PriceCardNewPriceLessThanZeroError { Message = $"Price {priceCard.NewPrice} for PriceCard {priceCard.Id} must be >= 0" };
            }
            finalPrice = ProductPrice.From(Convert.ToDecimal(priceCard.NewPrice)).Value;
        }

        return new ProductWithFinalPrice
        {
            Id = product.Id,
            Name = product.Name,
            ListPrice = product.ListPrice,
            FinalPrice = finalPrice
        };
    }
}

这种方式让实体更纯粹,专注于业务写逻辑,读逻辑完全独立,避免实体依赖外部服务。

方案3:只读属性+延迟加载(谨慎使用)

如果希望Product实体直接暴露FinalPrice,可以用延迟加载结合领域服务,但需要注意:

  • 实体不能直接持有外部服务客户端,而是通过注入的领域服务代理
  • 延迟加载仅在首次访问FinalPrice时触发外部调用

实现示例:

public class Product
{
    private readonly IProductPricingService _pricingService;
    private decimal? _finalPrice;

    public string Id { get; private set; }
    public string Name { get; private set; }
    public decimal ListPrice { get; private set; }

    // 仅在查询场景下创建实体时注入领域服务,写场景下不注入
    public Product(string id, string name, decimal listPrice, IProductPricingService pricingService = null)
    {
        Id = id;
        Name = name;
        ListPrice = listPrice;
        _pricingService = pricingService;
    }

    public async Task<decimal> GetFinalPrice(CancellationToken cancellationToken)
    {
        if (_finalPrice.HasValue)
        {
            return _finalPrice.Value;
        }

        if (_pricingService == null)
        {
            throw new InvalidOperationException("Pricing service not available for final price calculation");
        }

        _finalPrice = await _pricingService.CalculateFinalPrice(this, cancellationToken);
        return _finalPrice.Value;
    }
}

注意:这种方式会增加实体的复杂性,且写场景下创建实体时不能注入服务,需要严格区分实体的创建场景,适合对实体封装性要求极高的场景,但不推荐作为通用方案。

关键DDD原则总结

  • 实体隔离:实体不应依赖外部服务客户端,避免破坏领域模型的独立性和可测试性
  • 单一职责:实体专注于核心业务规则,跨边界逻辑交给领域服务处理
  • 读写分离:如果查询逻辑复杂且与写逻辑差异大,CQRS的读模型分离是更清晰的选择

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 07:05:34