如何优雅的将DTO转化成BO

本文转载自http://lrwinx.github.io

DTO

数据传输我们应该使用DTO对象作为传输对象,这是我们所约定的,因为很长时间我一直都在做移动端api设计的工作,有很多人告诉我,他们认为只有给手机端传输数据的时候(input or output),这些对象成为DTO对象。请注意!这种理解是错误的,只要是用于网络传输的对象,我们都认为他们可以当做是DTO对象,比如电商平台中,用户进行下单,下单后的数据,订单会发到OMS 或者 ERP系统,这些对接的返回值以及入参也叫DTO对象。

我们约定某对象如果是DTO对象,就将名称改为XXDTO,比如订单下发OMS:OMSOrderInputDTO。

DTO转化

正如我们所知,DTO为系统与外界交互的模型对象,那么肯定会有一个步骤是将DTO对象转化为BO对象或者是普通的entity对象,让service层去处理。

场景

比如添加会员操作,由于用于演示,我只考虑用户的一些简单数据,当后台管理员点击添加用户时,只需要传过来用户的姓名和年龄就可以了,后端接受到数据后,将添加创建时间和更新时间和默认密码三个字段,然后保存数据库。

@RequestMapping("/v1/api/user")  
@RestController  
public class UserApi {  
  
    @Autowired  
    private UserService userService;  
  
    @PostMapping  
    public User addUser(UserInputDTO userInputDTO){  
        User user = new User();  
        user.setUsername(userInputDTO.getUsername());  
        user.setAge(userInputDTO.getAge());  
  
        return userService.addUser(user);  
    }  
}
我们只关注一下上述代码中的转化代码,其他内容请忽略:
[java]  view plain  copy
  1. User user = new User();  
  2. user.setUsername(userInputDTO.getUsername());  
  3. user.setAge(userInputDTO.getAge());  

请使用工具

上边的代码,从逻辑上讲,是没有问题的,只是这种写法让我很厌烦,例子中只有两个字段,如果有20个字段,我们要如何做呢? 一个一个进行set数据吗?当然,如果你这么做了,肯定不会有什么问题,但是,这肯定不是一个最优的做法。

网上有很多工具,支持浅拷贝或深拷贝的Utils. 举个例子,我们可以使用org.springframework.beans.BeanUtils#copyProperties对代码进行重构和优化:

[java]  view plain  copy
  1. @PostMapping  
  2. public User addUser(UserInputDTO userInputDTO){  
  3.     User user = new User();  
  4.     BeanUtils.copyProperties(userInputDTO,user);  
  5.   
  6.     return userService.addUser(user);  
  7. }  
BeanUtils.copyProperties是一个浅拷贝方法,复制属性时,我们只需要把DTO对象和要转化的对象两个的属性值设置为一样的名称,并且保证一样的类型就可以了。如果你在做DTO转化的时候一直使用set进行属性赋值,那么请尝试这种方式简化代码,让代码更加清晰!

转化的语义

上边的转化过程,读者看后肯定觉得优雅很多,但是我们再写java代码时,更多的需要考虑语义的操作,再看上边的代码:

[java]  view plain  copy
  1. User user = new User();  
  2. BeanUtils.copyProperties(userInputDTO,user);  
虽然这段代码很好的简化和优化了代码,但是他的语义是有问题的,我们需要提现一个转化过程才好,所以代码改成如下:
[java]  view plain  copy
  1. @PostMapping  
  2.  public User addUser(UserInputDTO userInputDTO){  
  3.          User user = convertFor(userInputDTO);  
  4.   
  5.          return userService.addUser(user);  
  6.  }  
  7.   
  8.  private User convertFor(UserInputDTO userInputDTO){  
  9.   
  10.          User user = new User();  
  11.          BeanUtils.copyProperties(userInputDTO,user);  
  12.          return user;  
  13.  }  
这是一个更好的语义写法,虽然他麻烦了些,但是可读性大大增加了,在写代码时,我们应该尽量把语义层次差不多的放到一个方法中,比如:
[html]  view plain  copy
  1. User user = convertFor(userInputDTO);  
  2. return userService.addUser(user);  

这两段代码都没有暴露实现,都是在讲如何在同一个方法中,做一组相同层次的语义操作,而不是暴露具体的实现。如上所述,是一种重构方式,读者可以参考Martin Fowler的《Refactoring Imporving the Design of Existing Code》(重构 改善既有代码的设计) 这本书中的Extract Method重构方式。

抽象接口定义

当实际工作中,完成了几个api的DTO转化时,我们会发现,这样的操作有很多很多,那么应该定义好一个接口,让所有这样的操作都有规则的进行。
如果接口被定义以后,那么convertFor这个方法的语义将产生变化,他将是一个实现类。

看一下抽象后的接口:

[java]  view plain  copy
  1. public interface DTOConvert<S,T> {  
  2.     T convert(S s);  
  3. }  

虽然这个接口很简单,但是这里告诉我们一个事情,要去使用泛型,如果你是一个优秀的java程序员,请为你想做的抽象接口,做好泛型吧。我们再来看接口实现:

[java]  view plain  copy
  1. public class UserInputDTOConvert implements DTOConvert {  
  2.     @Override  
  3.     public User convert(UserInputDTO userInputDTO) {  
  4.         User user = new User();  
  5.         BeanUtils.copyProperties(userInputDTO,user);  
  6.         return user;  
  7.     }  
  8. }  
们这样重构后,我们发现现在的代码是如此的简洁,并且那么的规范:
[java]  view plain  copy
  1. @RequestMapping("/v1/api/user")  
  2. @RestController  
  3. public class UserApi {  
  4.   
  5.     @Autowired  
  6.     private UserService userService;  
  7.   
  8.     @PostMapping  
  9.     public User addUser(UserInputDTO userInputDTO){  
  10.         User user = new UserInputDTOConvert().convert(userInputDTO);  
  11.   
  12.         return userService.addUser(user);  
  13.     }  
  14. }  

review code

如果你是一个优秀的java程序员,我相信你应该和我一样,已经数次重复review过自己的代码很多次了。
我们再看这个保存用户的例子,你将发现,api中返回值是有些问题的,问题就在于不应该直接返回User实体,因为如果这样的话,就暴露了太多实体相关的信息,这样的返回值是不安全的,所以我们更应该返回一个DTO对象,我们可称它为UserOutputDTO:

[java]  view plain  copy
  1. @PostMapping  
  2. public UserOutputDTO addUser(UserInputDTO userInputDTO){  
  3.         User user = new UserInputDTOConvert().convert(userInputDTO);  
  4.         User saveUserResult = userService.addUser(user);  
  5.         UserOutputDTO result = new UserOutDTOConvert().convertToUser(saveUserResult);  
  6.         return result;  
  7. }  

这样你的api才更健全。不知道在看完这段代码之后,读者有是否发现还有其他问题的存在,作为一个优秀的java程序员,请看一下这段我们刚刚抽象完的代码:

User user = new UserInputDTOConvert().convert(userInputDTO);

你会发现,new这样一个DTO转化对象是没有必要的,而且每一个转化对象都是由在遇到DTO转化的时候才会出现,那我们应该考虑一下,是否可以将这个类和DTO进行聚合呢,看一下我的聚合结果:

public class UserInputDTO {
private String username;
private int age;

[java]  view plain  copy
  1. public String getUsername() {  
  2.     return username;  
  3. }  
  4.   
  5. public void setUsername(String username) {  
  6.     this.username = username;  
  7. }  
  8.   
  9. public int getAge() {  
  10.     return age;  
  11. }  
  12.   
  13. public void setAge(int age) {  
  14.     this.age = age;  
  15. }  
  16.   
  17.   
  18. public User convertToUser(){  
  19.     UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();  
  20.     User convert = userInputDTOConvert.convert(this);  
  21.     return convert;  
  22. }  
  23.   
  24. private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> {  
  25.     @Override  
  26.     public User convert(UserInputDTO userInputDTO) {  
  27.         User user = new User();  
  28.         BeanUtils.copyProperties(userInputDTO,user);  
  29.         return user;  
  30.     }  
  31. }  

然后api中的转化则由:
User user = new UserInputDTOConvert().convert(userInputDTO);
User saveUserResult = userService.addUser(user);

变成了:
User user = userInputDTO.convertToUser();
User saveUserResult = userService.addUser(user);

我们再DTO对象中添加了转化的行为,我相信这样的操作可以让代码的可读性变得更强,并且是符合语义的。

再查工具类

再来看DTO内部转化的代码,它实现了我们自己定义的DTOConvert接口,但是这样真的就没有问题,不需要再思考了吗?
我觉得并不是,对于Convert这种转化语义来讲,很多工具类中都有这样的定义,这中Convert并不是业务级别上的接口定义,它只是用于普通bean之间转化属性值的普通意义上的接口定义,所以我们应该更多的去读其他含有Convert转化语义的代码。
我仔细阅读了一下GUAVA的源码,发现了com.google.common.base.Convert这样的定义:

[java]  view plain  copy
  1. public abstract class Converter<A, B> implements Function<A, B> {  
  2.     protected abstract B doForward(A a);  
  3.     protected abstract A doBackward(B b);  
  4.     //其他略  
  5. }  
从源码可以了解到,GUAVA中的Convert可以完成正向转化和逆向转化,继续修改我们DTO中转化的这段代码:
[java]  view plain  copy
  1. private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> {  
  2.         @Override  
  3.         public User convert(UserInputDTO userInputDTO) {  
  4.                 User user = new User();  
  5.                 BeanUtils.copyProperties(userInputDTO,user);  
  6.                 return user;  
  7.         }  
  8. }  

修改后:

[java]  view plain  copy
  1. private static class UserInputDTOConvert extends Converter<UserInputDTO, User> {  
  2.          @Override  
  3.          protected User doForward(UserInputDTO userInputDTO) {  
  4.                  User user = new User();  
  5.                  BeanUtils.copyProperties(userInputDTO,user);  
  6.                  return user;  
  7.          }  
  8.   
  9.          @Override  
  10.          protected UserInputDTO doBackward(User user) {  
  11.                  UserInputDTO userInputDTO = new UserInputDTO();  
  12.                  BeanUtils.copyProperties(user,userInputDTO);  
  13.                  return userInputDTO;  
  14.          }  
  15.  }  

看了这部分代码以后,你可能会问,那逆向转化会有什么用呢?其实我们有很多小的业务需求中,入参和出参是一样的,那么我们变可以轻松的进行转化,我将上边所提到的UserInputDTO和UserOutputDTO都转成UserDTO展示给大家:

DTO:

[java]  view plain  copy
  1. public class UserDTO {  
  2.     private String username;  
  3.     private int age;  
  4.   
  5.     public String getUsername() {  
  6.             return username;  
  7.     }  
  8.   
  9.     public void setUsername(String username) {  
  10.             this.username = username;  
  11.     }  
  12.   
  13.     public int getAge() {  
  14.             return age;  
  15.     }  
  16.   
  17.     public void setAge(int age) {  
  18.             this.age = age;  
  19.     }  
  20.   
  21.   
  22.     public User convertToUser(){  
  23.             UserDTOConvert userDTOConvert = new UserDTOConvert();  
  24.             User convert = userDTOConvert.convert(this);  
  25.             return convert;  
  26.     }  
  27.   
  28.     public UserDTO convertFor(User user){  
  29.             UserDTOConvert userDTOConvert = new UserDTOConvert();  
  30.             UserDTO convert = userDTOConvert.reverse().convert(user);  
  31.             return convert;  
  32.     }  
  33.   
  34.     private static class UserDTOConvert extends Converter<UserDTO, User> {  
  35.             @Override  
  36.             protected User doForward(UserDTO userDTO) {  
  37.                     User user = new User();  
  38.                     BeanUtils.copyProperties(userDTO,user);  
  39.                     return user;  
  40.             }  
  41.   
  42.             @Override  
  43.             protected UserDTO doBackward(User user) {  
  44.                     UserDTO userDTO = new UserDTO();  
  45.                     BeanUtils.copyProperties(user,userDTO);  
  46.                     return userDTO;  
  47.             }  
  48.     }  
  49.   
  50. }  
api:
[java]  view plain  copy
  1. @PostMapping  
  2.  public UserDTO addUser(UserDTO userDTO){  
  3.          User user =  userDTO.convertToUser();  
  4.          User saveResultUser = userService.addUser(user);  
  5.          UserDTO result = userDTO.convertFor(saveResultUser);  
  6.          return result;  
  7.  }  
当然,上述只是表明了转化方向的正向或逆向,很多业务需求的出参和入参的DTO对象是不同的,那么你需要更明显的告诉程序:逆向是无法调用的:
[java]  view plain  copy
  1. private static class UserDTOConvert extends Converter<UserDTO, User> {  
  2.          @Override  
  3.          protected User doForward(UserDTO userDTO) {  
  4.                  User user = new User();  
  5.                  BeanUtils.copyProperties(userDTO,user);  
  6.                  return user;  
  7.          }  
  8.   
  9.          @Override  
  10.          protected UserDTO doBackward(User user) {  
  11.                  throw new AssertionError("不支持逆向转化方法!");  
  12.          }  
  13.  }  

看一下doBackward方法,直接抛出了一个断言异常,而不是业务异常,这段代码告诉代码的调用者,这个方法不是准你调用的,如果你调用,我就”断言”你调用错误了。

猜你喜欢

转载自blog.csdn.net/dylin83/article/details/79837659
今日推荐