本文轉載自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);
}
}
我們隻關注一下上述代碼中的轉化代碼,其他内容請忽略:
User user = new User();
user.setUsername(userInputDTO.getUsername());
user.setAge(userInputDTO.getAge());
請使用工具
上邊的代碼,從邏輯上講,是沒有問題的,隻是這種寫法讓我很厭煩,例子中隻有兩個字段,如果有20個字段,我們要如何做呢? 一個一個進行set資料嗎?當然,如果你這麼做了,肯定不會有什麼問題,但是,這肯定不是一個最優的做法。
網上有很多工具,支援淺拷貝或深拷貝的Utils. 舉個例子,我們可以使用org.springframework.beans.BeanUtils#copyProperties對代碼進行重構和優化:
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return userService.addUser(user);
}
BeanUtils.copyProperties是一個淺拷貝方法,複制屬性時,我們隻需要把DTO對象和要轉化的對象兩個的屬性值設定為一樣的名稱,并且保證一樣的類型就可以了。如果你在做DTO轉化的時候一直使用set進行屬性指派,那麼請嘗試這種方式簡化代碼,讓代碼更加清晰!
轉化的語義
上邊的轉化過程,讀者看後肯定覺得優雅很多,但是我們再寫java代碼時,更多的需要考慮語義的操作,再看上邊的代碼:
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
雖然這段代碼很好的簡化和優化了代碼,但是他的語義是有問題的,我們需要提現一個轉化過程才好,是以代碼改成如下:
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = convertFor(userInputDTO);
return userService.addUser(user);
}
private User convertFor(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
這是一個更好的語義寫法,雖然他麻煩了些,但是可讀性大大增加了,在寫代碼時,我們應該盡量把語義層次差不多的放到一個方法中,比如:
User user = convertFor(userInputDTO);
return userService.addUser(user);
這兩段代碼都沒有暴露實作,都是在講如何在同一個方法中,做一組相同層次的語義操作,而不是暴露具體的實作。如上所述,是一種重構方式,讀者可以參考Martin Fowler的《Refactoring Imporving the Design of Existing Code》(重構 改善既有代碼的設計) 這本書中的Extract Method重構方式。
抽象接口定義
當實際工作中,完成了幾個api的DTO轉化時,我們會發現,這樣的操作有很多很多,那麼應該定義好一個接口,讓所有這樣的操作都有規則的進行。
如果接口被定義以後,那麼convertFor這個方法的語義将産生變化,他将是一個實作類。
看一下抽象後的接口:
public interface DTOConvert<S,T> {
T convert(S s);
}
雖然這個接口很簡單,但是這裡告訴我們一個事情,要去使用泛型,如果你是一個優秀的java程式員,請為你想做的抽象接口,做好泛型吧。我們再來看接口實作:
public class UserInputDTOConvert implements DTOConvert {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
我們這樣重構後,我們發現現在的代碼是如此的簡潔,并且那麼的規範:
@RequestMapping("/v1/api/user")
@RestController
public class UserApi {
@Autowired
private UserService userService;
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);
return userService.addUser(user);
}
}
review code
如果你是一個優秀的java程式員,我相信你應該和我一樣,已經數次重複review過自己的代碼很多次了。
我們再看這個儲存使用者的例子,你将發現,api中傳回值是有些問題的,問題就在于不應該直接傳回User實體,因為如果這樣的話,就暴露了太多實體相關的資訊,這樣的傳回值是不安全的,是以我們更應該傳回一個DTO對象,我們可稱它為UserOutputDTO:
@PostMapping
public UserOutputDTO addUser(UserInputDTO userInputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);
User saveUserResult = userService.addUser(user);
UserOutputDTO result = new UserOutDTOConvert().convertToUser(saveUserResult);
return result;
}
這樣你的api才更健全。不知道在看完這段代碼之後,讀者有是否發現還有其他問題的存在,作為一個優秀的java程式員,請看一下這段我們剛剛抽象完的代碼:
User user = new UserInputDTOConvert().convert(userInputDTO);
你會發現,new這樣一個DTO轉化對象是沒有必要的,而且每一個轉化對象都是由在遇到DTO轉化的時候才會出現,那我們應該考慮一下,是否可以将這個類和DTO進行聚合呢,看一下我的聚合結果:
public class UserInputDTO {
private String username;
private int age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public User convertToUser(){
UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
User convert = userInputDTOConvert.convert(this);
return convert;
}
private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
}
然後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這樣的定義:
public abstract class Converter<A, B> implements Function<A, B> {
protected abstract B doForward(A a);
protected abstract A doBackward(B b);
//其他略
}
從源碼可以了解到,GUAVA中的Convert可以完成正向轉化和逆向轉化,繼續修改我們DTO中轉化的這段代碼:
private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
修改後:
private static class UserInputDTOConvert extends Converter<UserInputDTO, User> {
@Override
protected User doForward(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
@Override
protected UserInputDTO doBackward(User user) {
UserInputDTO userInputDTO = new UserInputDTO();
BeanUtils.copyProperties(user,userInputDTO);
return userInputDTO;
}
}
看了這部分代碼以後,你可能會問,那逆向轉化會有什麼用呢?其實我們有很多小的業務需求中,入參和出參是一樣的,那麼我們變可以輕松的進行轉化,我将上邊所提到的UserInputDTO和UserOutputDTO都轉成UserDTO展示給大家:
DTO:
public class UserDTO {
private String username;
private int age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public User convertToUser(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User convert = userDTOConvert.convert(this);
return convert;
}
public UserDTO convertFor(User user){
UserDTOConvert userDTOConvert = new UserDTOConvert();
UserDTO convert = userDTOConvert.reverse().convert(user);
return convert;
}
private static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
protected User doForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}
@Override
protected UserDTO doBackward(User user) {
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
return userDTO;
}
}
}
api:
@PostMapping
public UserDTO addUser(UserDTO userDTO){
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}
當然,上述隻是表明了轉化方向的正向或逆向,很多業務需求的出參和入參的DTO對象是不同的,那麼你需要更明顯的告訴程式:逆向是無法調用的:
private static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
protected User doForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}
@Override
protected UserDTO doBackward(User user) {
throw new AssertionError("不支援逆向轉化方法!");
}
}
看一下doBackward方法,直接抛出了一個斷言異常,而不是業務異常,這段代碼告訴代碼的調用者,這個方法不是準你調用的,如果你調用,我就”斷言”你調用錯誤了。