如何在考虑可扩展性的同时将域实体正确地转换为DTO。可测试性

时间:2017-12-16 06:05:14

标签: java spring spring-boot design-patterns junit

我已经阅读了几篇文章和Stackoverflow帖子,用于将域对象转换为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()在私有方法中使用,如果对象深度嵌套,我必须提供when(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject)的足够结果以避免NullPointers如果convertion也解散了嵌套结构

解决方案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的转换器或任何其他外置Bean进行此转换

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

优点:

  • 易于测试,因为我可以在测试用例期间模拟结果
  • 任务分离 - &gt;一个专门的班级正在做这项工作

缺点:

  • 在我的域模型增长时不会“扩展”那么多。有很多实体,我必须为每个新实体创建两个转换器( - &gt;转换DTO权利和授权给DTO)

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

提前致谢!

5 个答案:

答案 0 :(得分:11)

  

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

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

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

  

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

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

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

  

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

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

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

可以使用modelmapper等映射框架来完成实现。

您还说过每个实体的转换器

  随着我的域模型的增长,

并没有“扩展”。对于很多实体,我必须为每个新实体创建两个转换器( - &gt;转换DTO权利和授权给DTO)

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

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

如果我从专用或共享DTO的优点和缺点开始,这个答案将会很长,所以我只能请你阅读我的博客pros and cons of service layer designs

答案 1 :(得分:6)

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

  

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

我以这种方式创建DtoConverter

BaseEntity类标记:

public abstract class BaseEntity implements Serializable {
}

AbstractDto类标记:

public class AbstractDto {
}

GenericConverter界面:

public interface GenericConverter<D extends AbstractDto, E extends BaseEntity> {

    E createFrom(D dto);

    D createFrom(E entity);

    E updateEntity(E entity, D dto);

    default List<D> createFromEntities(final Collection<E> entities) {
        return entities.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

    default List<E> createFromDtos(final Collection<D> dtos) {
        return dtos.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

}

CommentConverter界面:

public interface CommentConverter extends GenericConverter<CommentDto, CommentEntity> {
}

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;
    }

}

答案 2 :(得分:4)

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

答案 3 :(得分:2)

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

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

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

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

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

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

答案 4 :(得分:1)

还有一点是,如果你使用第二种方法并且你的实体有惰性依赖,你的 Dto 无法理解是否加载了依赖,除非你将 EntityManager 注入到 Dto 并使用它来检查是否加载了依赖.我不喜欢这种方法,因为 Dto 不应该对 EntityManager 一无所知。作为一种解决方案,我个人更喜欢 Converters 但同时我更喜欢为同一个实体使用多个 Dto 类。例如,如果我 100% 确定 User Entity 将在没有相应 Company 的情况下加载,那么必须有一个 UserDto 没有 CompanyDto 作为字段。同时,如果我知道 UserEntity 将加载相关的 Company ,那么我将使用聚合模式,类似于包含 UserCompanyDto 和 {{ 1}} 作为参数