如何在考虑可伸缩性和可测试性的同时将域实体正确转换为DTO

我已经阅读了几篇文章和Stackoverflowpost,用于将域对象转换为DTO,并在我的代码中尝试了它们。 在测试和可扩展性方面,我总是面临一些问题。 我知道以下三种可能的解决方案,用于将域对象转换为DTO。 大部分时间我都在使用Spring。

解决方案1:服务层中用于转换的私有方法

第一种可能的解决方案是在服务层代码中创建一个小的“辅助”方法,该方法将检索到的数据库对象转换为我的DTO对象。

@Service public MyEntityService { public SomeDto getEntityById(Long id){ SomeEntity dbResult = someDao.findById(id); SomeDto dtoResult = convert(dbResult); // ... more logic happens return dtoResult; } public SomeDto convert(SomeEntity entity){ //... Object creation and using getter/setter for converting } } 

优点:

  • 易于实施
  • 没有额外的转换类需要 – >项目不会与实体爆炸

缺点:

  • 测试时出现问题,因为在new SomeEntity()方法中使用了new SomeEntity() ,并且如果对象是深层嵌套的,我必须提供足够的结果when(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject)以避免NullPointers如果转换也解散了嵌套结构

解决方案2:DTO中用于将域实体转换为DTO的附加构造函数

我的第二个解决方案是在我的DTO实体中添加一个额外的构造函数来转换构造函数中的对象。

 public class SomeDto { // ... some attributes public SomeDto(SomeEntity entity) { this.attribute = entity.getAttribute(); // ... nesting convertion & convertion of lists and arrays } } 

优点:

  • 没有额外的转换类需要
  • 转换隐藏在DTO实体中 – >服务代码较小

缺点:

  • 在服务代码中使用new SomeDto()因此我必须提供正确的嵌套对象结构作为我的someDao的结果。

解决方案3:使用Spring的Converter或任何其他外部化Bean进行此转换

如果最近看到Spring提供了一个转换原因的类: Converter但是这个解决方案代表正在进行转换的每个外化类。 有了这个解决方案,我将转换器注入我的服务代码,当我想将域实体转换为我的DTO时,我会调用它。

优点:

  • 我可以在测试用例中模拟结果,因此很容易测试
  • 任务分离 – >一个专门的class级正在完成这项工作

缺点:

  • 随着我的域模型的增长,不会“缩放”那么多。 有很多实体,我必须为每个新实体创建两个转换器( – >转换DTO权利和授权给DTO)

您是否有更多解决方案来解决我的问题,您如何处理? 您是否为每个新的域对象创建一个新的Converter并且可以“生活”项目中的类数量?

提前致谢!

解决方案1:服务层中用于转换的私有方法

我认为解决方案1不会有效,因为您的DTO是面向域的而不是面向服务的。 因此,很可能它们被用于不同的服务中。 因此映射方法不属于一个服务,因此不应在一个服务中实现。 您将如何在另一个服务中重用映射方法?

如果您按服务方法使用专用DTO,那么1.解决方案将很有效。 但最后更多关于这一点。

解决方案2:DTO中用于将域实体转换为DTO的附加构造函数

通常是一个很好的选择,因为您可以将DTO视为实体的适配器。 换句话说:DTO是实体的另一种表示。 这样的设计通常包装源对象并提供方法,为您提供包装对象的另一个视图。

但是DTO是一个数据传输对象,所以它可能迟早被序列化并通过网络发送,例如使用spring的远程处理function 。 在这种情况下,接收此DTO的客户端必须反序列化它,因此需要其类路径中的实体类,即使它只使用DTO的接口。

解决方案3:使用Spring的Converter或任何其他外部化Bean进行此转换

解决方案3是我也更喜欢的解决方案。 但我会创建一个Mapper接口,负责从源到目标的映射,反之亦然。 例如

 public interface Mapper { public T map(S source); public S map(T target); } 

可以使用像modelmapper这样的映射框架来完成实现。


你还说每个实体都有一个转换器

随着我的域模型的增长,不会“缩放”那么多。 有很多实体,我必须为每个新实体创建两个转换器( – >转换DTO权利和授权给DTO)

我认为你只需要为一个DTO创建2个转换器或一个映射器,因为你的DTO是面向域的。

一旦您开始在另一个服务中使用它,您将认识到其他服务通常应该或不能返回第一个服务所做的所有值。 您将开始为每个其他服务实现另一个映射器或转换器。

如果我从专用或共享DTO的优点和缺点开始,这个答案会很长,所以我只能请你阅读我的博客服务层设计的优缺点 。

在我看来,第三种解决方案是最好的解决方案。 是的,对于每个实体,您必须创建两个新的转换类,但是当您有时间进行测试时,您不会有太多麻烦。 您永远不应该选择能够在开始时编写更少代码的解决方案,然后在测试和维护代码时编写更多代码。

我喜欢接受的答案中的第三个解决方案。

解决方案3:使用Spring的Converter或任何其他外部化Bean进行此转换

我以这种方式创建DtoConverter

BaseEntity类标记:

 public abstract class BaseEntity implements Serializable { } 

AbstractDto类标记:

 public class AbstractDto { } 

GenericConverter接口:

 public interface GenericConverter { E createFrom(D dto); D createFrom(E entity); E updateEntity(E entity, D dto); default List createFromEntities(final Collection entities) { return entities.stream() .map(this::createFrom) .collect(Collectors.toList()); } default List createFromDtos(final Collection dtos) { return dtos.stream() .map(this::createFrom) .collect(Collectors.toList()); } } 

CommentConverter界面:

 public interface CommentConverter extends GenericConverter { } 

CommentConveter类实现:

 @Component public class CommentConverterImpl implements CommentConverter { @Override public CommentEntity createFrom(CommentDto dto) { CommentEntity entity = new CommentEntity(); updateEntity(entity, dto); return entity; } @Override public CommentDto createFrom(CommentEntity entity) { CommentDto dto = new CommentDto(); if (entity != null) { dto.setAuthor(entity.getAuthor()); dto.setCommentId(entity.getCommentId()); dto.setCommentData(entity.getCommentData()); dto.setCommentDate(entity.getCommentDate()); dto.setNew(entity.getNew()); } return dto; } @Override public CommentEntity updateEntity(CommentEntity entity, CommentDto dto) { if (entity != null && dto != null) { entity.setCommentData(dto.getCommentData()); entity.setAuthor(dto.getAuthor()); } return entity; } } 

我最终没有使用一些神奇的映射库或外部转换器类,只是添加了我自己的小bean,它将每个实体的方法convert为我需要的每个DTO。 原因是映射是:

要么是愚蠢的简单,我只是将一些值从一个字段复制到另一个字段,也许用一个小的实用方法,

或者相当复杂,将自定义参数写入某些通用映射库会比写出该代码更复杂。 例如,在客户端可以发送JSON但在引擎盖下它被转换为实体的情况下,当客户端再次检索这些实体的父对象时,它将转换回JSON。

这意味着我可以在任何实体集合上调用.map(converter::convert)来获取我的DTO流。

将它全部集中在一个类中是否可扩展? 那么即使使用通用映射器,也必须将此映射的自定义配置存储在某处。 代码通常非常简单,除了少数情况,所以我不太担心这个类在复杂性上爆炸。 我也没想到会有更多的实体,但如果我这样做,我可能会将这些转换器分组到每个子域的类中。

在我的实体和DTO中添加一个基类,这样我就可以编写一个通用的转换器接口,并且每个类实现它只是不需要(但是?)。