如何優雅的將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方法,直接拋出了一個斷言異常,而不是業務異常,這段代碼告訴代碼的調用者,這個方法不是準你調用的,如果你調用,我就”斷言”你調用錯誤了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章