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

Clean Architecture中DTO正确放置位置及循环依赖问题

Clean Architecture 中 DTO 放置位置与分层规范解答

你的分层理解核心偏差

  • 最根本的问题是将本属于Application层的用例服务接口错误定义在了Domain层。Clean Architecture的核心依赖规则是「外层依赖内层,内层不感知外层的任何概念」:Domain层作为最核心的内层,只允许存放和核心业务规则强绑定的内容——包括领域实体、值对象、领域枚举、操作领域实体的核心领域服务接口、仓储抽象接口,绝对不能引入任何和用例编排、持久化、外部接口相关的外层概念。
  • 你提到的RetrieveDto这类返回组合数据结构的方法,本质是面向特定用例的编排逻辑,不属于核心领域能力,对应的接口根本不应该定义在Domain层,自然不会出现Domain层需要引用Application层DTO的循环依赖问题。
  • 你之前尝试的「在Domain层定义抽象DTO基类、Application层DTO继承该基类」的方案完全没有必要,属于为了绕开依赖问题硬加的无意义抽象:DTO本身是无行为的纯数据结构,抽象层不存在需要约定的行为或契约,加了只会增加不必要的维护成本,违背了KISS原则,也不符合Clean Architecture的设计初衷。

DTO的分层放置规则

按照DTO的使用场景,对应放置位置非常明确:

  • Application层用例输入/输出DTO:也就是你当前使用的、组合多个领域实体、适配通用用例返回结构的纯数据对象,放在Application层是完全正确的。对应的用例服务接口也直接定义在Application层,服务实现同样放在Application层,依赖Domain层的实体、仓储抽象完成逻辑编排,在这一层完成领域实体到用例DTO的映射,完全符合依赖方向要求。你当前把对象映射器放在Application层的做法也是正确的,用例编排层本来就负责协调领域对象、输出用例需要的结构。
  • Presentation/API层接口DTO:这类DTO直接和外部对接系统的请求/响应格式绑定,应该单独放在API层。刚好适配你需要给多个对接系统返回不同结构响应的需求:每个对接系统的专属请求、响应DTO都在API层单独定义,接口收到请求后先把API层DTO转换成Application层的用例入参,调用Application层服务拿到用例输出DTO后,再转换成对应系统需要的响应格式返回即可,不需要改动Application、Domain层的任何代码。
  • Infrastructure层持久化DTO:如果需要对接遗留数据库、存在和领域实体结构不一致的存储格式,对应的数据库实体类放在Infrastructure层即可,仓储实现类负责把数据库查询出来的持久化DTO转换成Domain层的领域实体返回,上层完全感知不到遗留存储格式的存在。

修正后的最小可运行项目结构参考

Domain层(无任何外层依赖)

// 核心领域实体
public class EntityA
{
    public string Prop1{get; set;}
}
public class EntityB
{
    public string Prop2{get; set;}
}

// 核心领域服务接口,入参出参只能是Domain层内部的类型
public interface IDomainService
{
    void ExecuteCoreBusinessLogic(EntityA a, EntityB b);
}

// 仓储抽象接口
public interface IEntityARepository
{
    Task<EntityA> GetByIdAsync(string id);
}
public interface IEntityBRepository
{
    Task<EntityB> GetRelatedByAIdAsync(string aId);
}

Application层(仅依赖Domain层)

// 用例服务接口,定义在Application层
public interface IRetrieveCombinedDataUseCase
{
    Task<IEnumerable<CombinedDataDto>> RunAsync(string filter);
}

// 用例输出DTO,直接定义在Application层
public class CombinedDataDto
{
    public string Prop1{get; set;}
    public string Prop2{get; set;}
}

// 用例实现
public class RetrieveCombinedDataUseCase : IRetrieveCombinedDataUseCase
{
    private readonly IEntityARepository _aRepo;
    private readonly IEntityBRepository _bRepo;

    public RetrieveCombinedDataUseCase(IEntityARepository aRepo, IEntityBRepository bRepo)
    {
        _aRepo = aRepo;
        _bRepo = bRepo;
    }

    public async Task<IEnumerable<CombinedDataDto>> RunAsync(string filter)
    {
        // 调用仓储拿领域实体
        var entityA = await _aRepo.GetByIdAsync(filter);
        var entityB = await _bRepo.GetRelatedByAIdAsync(entityA.Prop1);
        // 映射为用例DTO返回
        return new List<CombinedDataDto>
        {
            new CombinedDataDto { Prop1 = entityA.Prop1, Prop2 = entityB.Prop2 }
        };
    }
}

Infrastructure层(依赖Domain层,实现Domain层定义的抽象)

// 遗留数据库对应的持久化DTO,仅在Infrastructure层内部使用
internal class LegacyEntityADbDto
{
    public string legacy_col_1 {get;set;} // 遗留库不规则字段
}

public class EntityARepository : IEntityARepository
{
    // 实现仓储逻辑:查询遗留库 -> 把持久化DTO转换为Domain层EntityA返回
}

API层(依赖Application层,处理外部请求)

// 给对接系统A的专属响应DTO
public class SystemADataResponse
{
    public string DataA {get;set;}
    public string DataB {get;set;}
}

// 给对接系统B的专属响应DTO
public class SystemBDataResponse
{
    public string Field1 {get;set;}
    // 系统B不需要Prop2字段可以直接不定义
}

[ApiController]
[Route("api/v1/system-a/data")]
public class SystemADataController : ControllerBase
{
    private readonly IRetrieveCombinedDataUseCase _useCase;

    public SystemADataController(IRetrieveCombinedDataUseCase useCase)
    {
        _useCase = useCase;
    }

    [HttpGet]
    public async Task<IEnumerable<SystemADataResponse>> Get([FromQuery] string filter)
    {
        var appLayerDto = await _useCase.RunAsync(filter);
        // 转换为系统A需要的格式返回
        return appLayerDto.Select(x => new SystemADataResponse
        {
            DataA = x.Prop1,
            DataB = x.Prop2
        });
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.31 22:06:19