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

DDD实践:实体、契约与DTO的映射逻辑应置于何处?

映射逻辑的位置选择与DateDeleted权限控制方案

一、映射逻辑的常规做法

  • 外部Mapper类是主流选择:这符合单一职责原则——领域实体Document应该专注于封装领域行为、业务规则(比如你的守卫子句),而数据转换(DTO/请求响应契约与实体的映射)属于基础设施层的职责,交给外部Mapper类处理更清晰。尤其是当映射规则复杂、涉及多个实体,或者需要结合MapStruct、ModelMapper这类映射框架时,外部Mapper的优势更明显。
  • 实体内部放映射的场景有限:只有当映射逻辑完全依赖实体内部状态,且逻辑极简时才考虑,比如实体提供toDTO()方法生成对应的DTO(因为这是实体自身数据的导出)。但反向映射(DTO→实体)不建议放在实体内部,因为实体的创建应该由领域规则驱动,而非外部数据直接决定。

二、解决DateDeleted的权限控制问题(核心痛点)

你当前的矛盾是:既要让外部Mapper完成DTO到实体的映射,又不想暴露DateDeleted的公开设置器/构造器,避免外部随意修改这个属性。这里有几个更优的方案:

1. 给实体添加有界的工厂方法

这是最符合DDD封装原则的方案:在Document内部定义一个静态工厂方法,专门负责从DTO恢复实体,在方法内部合法设置DateDeleted,同时封装必要的业务验证。外部Mapper只需要调用这个工厂方法,无需直接操作实体的私有属性。

示例代码:

public class Document {
    private LocalDateTime dateDeleted;
    // 其他私有属性

    // 私有构造器,确保实体只能通过内部方法创建
    private Document() {}

    // 工厂方法:从DTO恢复实体,处理DateDeleted的设置逻辑
    public static Document fromDTO(DocumentDTO dto) {
        Document doc = new Document();
        // 映射其他属性,同时应用守卫子句验证
        doc.setTitle(dto.getTitle());
        doc.setContent(dto.getContent());
        // 内部合法设置DateDeleted,加入领域规则验证
        if (dto.getDateDeleted() != null) {
            // 比如:只有当DTO标记文档为已删除时,才允许设置这个值
            if (dto.isDeleted()) {
                doc.dateDeleted = dto.getDateDeleted();
            } else {
                throw new IllegalArgumentException("未删除的文档不能设置DateDeleted");
            }
        }
        return doc;
    }

    // 其他领域行为方法
    private void setTitle(String title) {
        // 守卫子句:标题不能为空
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("文档标题不能为空");
        }
        this.title = title;
    }
}

Mapper类中只需调用Document.fromDTO(dto)即可完成映射,完全不用暴露实体的私有构造或属性。

2. 使用包级访问权限

把Document和Mapper类放在同一个包下,给实体添加包级别的构造器或设置方法(即不添加public修饰符)。这样同包的Mapper可以调用这些方法,而外部其他包的类无法访问,在一定程度上保留封装性。这种方式适合逻辑简单、不需要复杂验证的场景。

3. 结合映射框架的自定义转换器

如果使用MapStruct这类映射框架,可以在Mapper接口中定义自定义转换逻辑,调用实体的工厂方法或内部方法完成DateDeleted的设置,避免直接操作私有属性。示例:

@Mapper
public interface DocumentMapper {
    DocumentMapper INSTANCE = Mappers.getMapper(DocumentMapper.class);

    // 忽略自动映射DateDeleted,交给自定义逻辑处理
    @Mapping(target = "dateDeleted", ignore = true)
    Document mapBasicFields(DocumentDTO dto);

    // 自定义转换方法,调用实体工厂方法
    default Document mapFromDTO(DocumentDTO dto) {
        return Document.fromDTO(dto);
    }
}

总结

  • 尽量保持映射逻辑的外部化,避免让领域实体承担数据转换的职责,防止实体职责膨胀。
  • 优先选择工厂方法的方案,既保留了实体对内部状态的控制权,又能优雅完成DTO到实体的映射,完全符合DDD的封装原则。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 05:15:32