天天看點

如何優雅的将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);
    }
}
           

我們隻關注一下上述代碼中的轉化代碼,其他内容請忽略:

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

繼續閱讀