领域驱动设计:调用外部服务丰富实体属性的可行方案探讨
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中不建议直接将外部服务客户端注入实体(会破坏实体的独立性和可测试性),更推荐用领域服务封装跨边界的逻辑:
- 定义领域服务接口
IProductPricingService,负责计算FinalPrice:
public interface IProductPricingService { Task<decimal> CalculateFinalPrice(Product product, CancellationToken cancellationToken); }
- 实现该接口,注入
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; } }
- 在请求处理器中注入领域服务,调用计算逻辑:
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 }; } } }
- 调整
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,负责业务规则和持久化 - 读模型:创建
ProductWithFinalPriceDTO,专门用于查询场景,在查询时直接聚合外部服务数据
实现步骤:
- 写模型
Product去掉FinalPrice属性:
public class Product { public string Id { get; private set; } public string Name { get; private set; } public decimal ListPrice { get; private set; } // 构造函数、业务方法... }
- 定义读模型DTO:
public class ProductWithFinalPrice { public string Id { get; set; } public string Name { get; set; } public decimal ListPrice { get; set; } public decimal FinalPrice { get; set; } }
- 查询处理器直接聚合数据:
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
相关产品推荐
相关产品推荐

