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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/47843039/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 09:50:34  来源:igfitidea点击:

How to properly convert domain entities to DTOs while considering scalability & testability

javaspringspring-bootdesign-patternsjunit

提问by rieckpil

I have read several articles and Stackoverflow posts for converting domain objects to DTOs and tried them out in my code. When it comes to testing and scalability I am always facing some issues. I know the following three possible solutions for converting domain objects to DTOs. Most of the time I am using Spring.

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

Solution 1: Private method in the service layer for converting

方案一:服务层的私有方法进行转换

The first possible solution is to create a small "helper" method in the service layer code which is convertig the retrieved database object to my DTO object.

第一个可能的解决方案是在服务层代码中创建一个小的“帮助器”方法,它将检索到的数据库对象转换为我的 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
  }
}

Pros:

优点:

  • easy to implement
  • no additional class for convertion needed -> project doesn't blow up with entities
  • 易于实施
  • 不需要额外的转换类 -> 项目不会因实体而爆炸

Cons:

缺点:

  • problems when testing, as new SomeEntity()is used in the privated method and if the object is deeply nested I have to provide a adequate result of my when(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject)to avoid NullPointers if convertion is also dissolving the nested structure
  • 测试时出现问题,如new SomeEntity()私有方法中使用的那样,如果对象嵌套很深,我必须提供足够的结果when(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject)以避免 NullPointers 如果转换也溶解嵌套结构

Solution 2: Additional constructor in the DTO for converting domain entity to DTO

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

My second solution would be to add an additional constructor to my DTO entity to convert the object in the constructor.

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

public class SomeDto {

 // ... some attributes

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

}

Pros:

优点:

  • no additional class for converting needed
  • convertion hided in the DTO entity -> service code is smaller
  • 不需要额外的转换类
  • 转换隐藏在 DTO 实体中 -> 服务代码更小

Cons:

缺点:

  • usage of new SomeDto()in the service code and therefor I have to provide the correct nested object structure as a result of my someDaomocking.
  • 的使用new SomeDto()在服务代码,并为此我必须提供正确的嵌套对象结构作为我的结果someDao嘲讽。

Solution 3: Using Spring's Converter or any other externalized Bean for this converting

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

If recently saw that Spring is offering a class for converting reasons: Converter<S, T>but this solution stands for every externalized class which is doing the convertion. With this solution I am injecting the converter to my service code and I call it when i want to convert the domain entity to my DTO.

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

Pros:

优点:

  • easy to test as I can mock the result during my test case
  • separation of tasks -> a dedicated class is doing the job
  • 易于测试,因为我可以在测试用例中模拟结果
  • 任务分离 -> 一个专门的班级正在做这项工作

Cons:

缺点:

  • doesn't "scale" that much as my domain model grows. With a lot of entities I have to create two converters for every new entity (-> converting DTO entitiy and entitiy to DTO)
  • 随着我的域模型的增长,不会“扩展”那么多。对于很多实体,我必须为每个新实体创建两个转换器(-> 将 DTO 实体和实体转换为 DTO)

Do you have more solutions for my problem and how do you handle it? Do you create a new Converter for every new domain object and can "live" with the amount of classes in the project?

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

Thanks in advance!

提前致谢!

采纳答案by René Link

Solution 1: Private method in the service layer for converting

方案一:服务层的私有方法进行转换

I guess Solution 1will not not work well, because your DTOs are domain-oriented and not service oriented. Thus it will be likely that they are used in different services. So a mapping method does not belong to one servce and therefore should not be implemented in one service. How would you re-use the mapping method in another service?

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

The 1. solution would work well if you use dedicated DTOs per service method. But more about this at the end.

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

Solution 2: Additional constructor in the DTO for converting domain entity to DTO

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

In general a good option, because you can see the DTO as an adapter to the entity. In other words: the DTO is another representation of an entity. Such designs often wrap the source object and provide methods that give you another view on the wrapped object.

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

But a DTO is a data transferobject so it might be serialized sooner or later and send over a network, e.g. using spring's remoting capabilities. In this case the client that receives this DTO must deserialize it and thus needs the entity classes in it's classpath, even if it only uses the DTO's interface.

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

Solution 3: Using Spring's Converter or any other externalized Bean for this converting

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

Solution 3 is the solution that I also would prefer. But I would create a Mapper<S,T>interface that is responsible for mapping from source to target and vice versa. E.g.

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

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

The implementation can be done using a mapping framework like modelmapper.

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



You also said that a converter for each entity

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

doesn't "scale" that much as my domain model grows. With a lot of entities I have to create two converters for every new entity (-> converting DTO entitiy and entitiy to DTO)

随着我的域模型的增长,不会“扩展”那么多。对于很多实体,我必须为每个新实体创建两个转换器(-> 将 DTO 实体和实体转换为 DTO)

I doupt that you only have to create 2 converter or one mapper for one DTO, because your DTO is domain-oriented.

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

As soon as you start to use it in another service you will recognize that the other service usually should or can not return all values that the first service does. You will start to implement another mapper or converter for each other service.

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

This answer would get to long if I start with pros and cons of dedicated or shared DTOs, so I can only ask you to read my blog pros and cons of service layer designs.

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

EDIT

编辑

About the third solution: where do you prefer to put the call for the mapper?

关于第三个解决方案:您更喜欢在哪里调用映射器?

In the layer above the use cases. DTOs are data transfer objects, because they pack data in data structures that are best for the transfer protocol. Thus I call that layer the transport layer. This layer is responsible for mapping use case's request and result objects from and to the transport representation, e.g. json data structures.

在用例之上的层。DTO 是数据传输对象,因为它们将数据打包在最适合传输协议的数据结构中。因此我称该层为传输层。该层负责将用例的请求和结果对象映射到传输表示,例如 json 数据结构。

回答by Maksym Pecheniuk

I like the third solution from the accepted answer.

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

Solution 3: Using Spring's Converter or any other externalized Bean for this converting

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

And I create DtoConverterin this way:

BaseEntity class marker:

DtoConverter以这种方式创建:

BaseEntity 类标记:

public abstract class BaseEntity implements Serializable {
}

AbstractDto class marker:

AbstractDto 类标记:

public class AbstractDto {
}

GenericConverter interface:

通用转换器接口:

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 interface:

CommentConverter 接口:

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

CommentConveter class implementation:

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

}

回答by Petar Petrov

In my opinion the third solution is the best one. Yes for each entity you'll have to create a two new convert classes but when you come time for testing you won't have a lot of headaches. You should never chose the solution which will cause you to write less code at the begining and then write much more when it comes to testing and maintaining that code.

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

回答by Sebastiaan van den Broek

I ended up NOT using some magical mapping library or external converter class, but just adding a small bean of my own which has convertmethods from each entity to each DTO I need. The reason is that the mapping was:

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

eitherstupidly simple and I would just copy some values from one field to another, perhaps with a small utility method,

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

orwas quite complex and would be more complicated to write down in the custom parameters to some generic mapping library, compared to just writing out that code. This is for example in the case where the client can send JSON but under the hood this is transformed into entities, and when the client retrieves the parent object of these entities again, it's converted back into JSON.

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

This means I can just call .map(converter::convert)on any collection of entities to get back a stream of my DTO's.

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

Is it scalable to have it all in one class? Well the custom configuration for this mapping would have to be stored somewhere even if using a generic mapper. The code is generally extremely simple, except for a handful of cases, so I'm not too worried about this class exploding in complexity. I'm also not expecting to have dozens more entities, but if I did I might group these converters in a class per subdomain.

将所有内容集中在一个课程中是否可扩展?好吧,即使使用通用映射器,此映射的自定义配置也必须存储在某处。除了少数情况外,代码通常非常简单,所以我不太担心这个类的复杂性会爆炸式增长。我也不希望有更多的实体,但如果我这样做了,我可能会将这些转换器分组在每个子域的一个类中。

Adding a base class to my entities and DTO's so I can write a generic converter interface and implement it per class just isn't needed (yet?) either for me.

向我的实体和 DTO 添加一个基类,这样我就可以编写一个通用转换器接口并按类实现它,这对我来说也不需要(还没有?)。