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
相关产品推荐
相关产品推荐

