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

我已经阅读了几篇关于将域对象转换为DTO的文章和Stackoverflow文章,并在我的代码中尝试了它们。在测试和可伸缩性方面,我总是面临一些问题。我知道以下三种将域对象转换为DTO的可能解决方案。大多数时候,我都在使用春天。

解决方案 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
  }
}

优点:

  • 易于实施
  • 不需要额外的转换类 - >项目不会与实体一起爆炸

缺点:

  • 测试时出现问题,就像在私有方法中使用的一样,如果对象是深度嵌套的,我必须提供足够的结果,以避免NullPointers,如果转换也溶解了嵌套结构new SomeEntity()when(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject)

解决方案 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出于转换原因提供了一个类:但是这个解决方案代表了每个进行转换的外部化类。使用此解决方案,我将转换器注入到我的服务代码中,当我想将域实体转换为我的DTO时,我会调用它。Converter<S, T>

优点:

  • 易于测试,因为我可以在测试用例中模拟结果
  • 任务分离 - >专门的类正在完成工作

缺点:

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

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

提前致谢!


答案 1

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

我想解决方案 1 不会很好地工作,因为您的 DTO 是面向域的,而不是面向服务的。因此,它们很可能用于不同的服务。因此,映射方法不属于一个服务,因此不应在一个服务中实现。如何在其他服务中重用映射方法?

该 1.如果对每个服务方法使用专用的 DTO,解决方案将运行良好。但最后会对此进行更多介绍。

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

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

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

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

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

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

可以使用模型映射器等映射框架来完成实现。


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

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

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

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

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

编辑

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

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

编辑

我看到你可以将实体作为DTO构造函数参数传递。你也会对相反的情况感到满意吗?我的意思是,将 DTO 作为实体构造函数参数传递?

一个好问题。相反的情况对我来说是不行的,因为这样我就会在实体中引入对传输层的依赖关系。这意味着传输层中的更改可能会影响实体,我不希望更详细层中的更改影响更抽象的层。

如果需要将数据从传输层传递到实体层,则应应用依赖关系反转原则。

引入一个将通过一组 getter 返回数据的接口,让 DTO 实现它并在实体构造函数中使用此接口。请记住,此接口属于实体的层,因此不应与传输层有任何依赖关系。

                                interface
 +-----+  implements     ||   +------------+   uses  +--------+
 | DTO |  ---------------||-> | EntityData |  <----  | Entity |
 +-----+                 ||   +------------+         +--------+

答案 2

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

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

我以这种方式创建:

基实体类标记:DtoConverter

public abstract class BaseEntity implements Serializable {
}

摘要到类标记:

public class AbstractDto {
}

通用转换器接口:

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

}

注释转换器界面:

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

}

推荐