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

基于HTTP API持久化实现DDD关联/子实体/聚合的架构设计疑问

基于HTTP API持久化实现DDD关联/子实体/聚合的架构设计疑问

我的场景与问题

我们用C#开发REST Web API,采用了类似整洁架构/洋葱架构的分层模式,包含应用层、领域层和基础设施层。我想在项目中融入DDD理念,让领域模型真正体现领域结构。

不过我们的持久化/基础设施层并不是像很多示例那样用Entity Framework直接操作数据库,而是通过调用其他API来实现数据查询,而且我们还为一些我认为不属于聚合根的类型创建了仓储。

问题1

假设我们有一个可识别的聚合根模型System,SystemRepository通过HTTP调用API 1来获取System实例。我现在遇到的问题是,想在System类上添加一个GenerateReport()方法,用于为该系统生成一份Report。

Report不是实体,需要调用API 2的数据来构建。我希望把对API 2的HTTP调用放在基础设施层,其他层不需要关心数据存储是HTTP接口还是数据库。我考虑过几种方案,但不确定哪种更符合DDD和分层架构的原则。


我的专业建议

首先得给你锚定几个核心原则,再对应你的场景拆解可行方案:

核心原则先明确

  • 领域层只负责表达领域逻辑,绝对不能依赖基础设施层的具体实现(比如HTTP调用、数据库操作)
  • 聚合根的行为要内聚,但如果需要外部数据,不能直接去调用,得通过依赖反转把能力注入进来
  • 仓储只应该为聚合根服务,非聚合根的数据获取不要用仓储,改用数据服务/提供者的方式

针对GenerateReport()场景的最优方案:领域服务+抽象数据提供者

这个方案完全贴合DDD和洋葱架构的分层要求,把HTTP调用彻底隔离在基础设施层,同时保持领域层的纯净:

  1. 领域层:定义抽象与领域逻辑
    先在领域层定义获取报告数据的抽象接口,以及Report值对象和负责生成报告的领域服务——这里的核心是,领域层只关心“需要什么数据”,不关心“数据从哪来”:
// 领域层 - Report值对象(无状态,仅承载数据)
public record Report(System System, string FormattedMetrics, DateTime GeneratedAt);

// 领域层 - 抽象数据提供者(定义获取报告所需数据的契约)
public interface IReportMetricsProvider
{
    Task<ReportMetrics> GetMetricsForSystem(Guid systemId);
}

// 领域层 - 领域服务(封装报告生成的领域规则)
public class ReportGenerationService
{
    private readonly IReportMetricsProvider _metricsProvider;

    public ReportGenerationService(IReportMetricsProvider metricsProvider)
    {
        _metricsProvider = metricsProvider;
    }

    public async Task<Report> GenerateFor(System system)
    {
        // 先执行领域规则校验:仅活跃系统可生成报告
        if (!system.IsActive)
            throw new InvalidOperationException("仅活跃系统可生成报告");
        
        // 通过抽象获取外部数据
        var metrics = await _metricsProvider.GetMetricsForSystem(system.Id);
        // 封装成Report值对象返回
        return new Report(system, metrics.FormatForReport(), DateTime.UtcNow);
    }
}
  1. 基础设施层:实现数据提供者
    在基础设施层实现IReportMetricsProvider,这里面封装调用API 2的HTTP逻辑,彻底隔离上层与外部API的耦合:
// 基础设施层 - 基于API2的实现
public class Api2ReportMetricsProvider : IReportMetricsProvider
{
    private readonly HttpClient _httpClient;

    public Api2ReportMetricsProvider(HttpClient httpClient)
    {
        _httpClient = httpClient;
        // 可以在这里配置API2的基础地址等
        _httpClient.BaseAddress = new Uri("https://api2.example.com/");
    }

    public async Task<ReportMetrics> GetMetricsForSystem(Guid systemId)
    {
        var response = await _httpClient.GetAsync($"v1/system-metrics/{systemId}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<ReportMetrics>();
    }
}
  1. 应用层:协调流程
    应用层负责串联整个流程:从仓储获取System聚合根,再调用领域服务生成报告,最后返回结果给API控制器:
// 应用层 - 命令/查询处理器
public class GenerateSystemReportHandler
{
    private readonly ISystemRepository _systemRepo;
    private readonly ReportGenerationService _reportService;

    public GenerateSystemReportHandler(ISystemRepository systemRepo, ReportGenerationService reportService)
    {
        _systemRepo = systemRepo;
        _reportService = reportService;
    }

    public async Task<Report> Handle(Guid systemId)
    {
        var system = await _systemRepo.GetByIdAsync(systemId);
        if (system == null)
            throw new KeyNotFoundException("未找到指定系统");
        
        return await _reportService.GenerateFor(system);
    }
}

备选方案:聚合根注入依赖(仅适合简单场景)

如果你的报告生成逻辑非常简单,且和System的绑定度极高,可以考虑在System聚合根的构造函数中注入IReportMetricsProvider,让聚合根直接拥有生成报告的能力。但不推荐复杂场景使用,因为会增加聚合根的测试复杂度,也容易破坏聚合的边界:

// 领域层 - 聚合根(仅简单场景用)
public class System
{
    private readonly IReportMetricsProvider _metricsProvider;
    public Guid Id { get; }
    public bool IsActive { get; }

    public System(Guid id, bool isActive, IReportMetricsProvider metricsProvider)
    {
        Id = id;
        IsActive = isActive;
        _metricsProvider = metricsProvider;
    }

    public async Task<Report> GenerateReport()
    {
        if (!IsActive)
            throw new InvalidOperationException("仅活跃系统可生成报告");
        
        var metrics = await _metricsProvider.GetMetricsForSystem(Id);
        return new Report(this, metrics.FormatForReport(), DateTime.UtcNow);
    }
}

关于“非聚合根仓储”的修正建议

你提到为非聚合根创建了仓储,这其实违反了DDD的仓储原则。正确的做法是:

  • 非聚合根的数据应该通过所属的聚合根来获取(比如System下的Component,通过System.GetComponentById()获取)
  • 如果某些非聚合根数据确实需要独立查询,不要创建仓储,而是创建数据服务(比如IComponentDataService),在基础设施层实现为调用对应API的逻辑,然后在应用层或领域服务中使用。

备注:内容来源于stack exchange,提问作者Viktor

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 14:58:07