本文轉(zhuǎn)載自http://lrwinx.
DTO
數(shù)據(jù)傳輸我們應(yīng)該使用DTO對(duì)象作為傳輸對(duì)象,這是我們所約定的,因?yàn)楹荛L(zhǎng)時(shí)間我一直都在做移動(dòng)端api設(shè)計(jì)的工作,,有很多人告訴我,,他們認(rèn)為只有給手機(jī)端傳輸數(shù)據(jù)的時(shí)候(input or output),,這些對(duì)象成為DTO對(duì)象,。請(qǐng)注意!這種理解是錯(cuò)誤的,,只要是用于網(wǎng)絡(luò)傳輸?shù)膶?duì)象,,我們都認(rèn)為他們可以當(dāng)做是DTO對(duì)象,比如電商平臺(tái)中,,用戶進(jìn)行下單,,下單后的數(shù)據(jù),訂單會(huì)發(fā)到OMS 或者 ERP系統(tǒng),,這些對(duì)接的返回值以及入?yún)⒁步蠨TO對(duì)象。
我們約定某對(duì)象如果是DTO對(duì)象,,就將名稱改為XXDTO,比如訂單下發(fā)OMS:OMSOrderInputDTO,。
DTO轉(zhuǎn)化
正如我們所知,DTO為系統(tǒng)與外界交互的模型對(duì)象,,那么肯定會(huì)有一個(gè)步驟是將DTO對(duì)象轉(zhuǎn)化為BO對(duì)象或者是普通的entity對(duì)象,,讓service層去處理。
場(chǎng)景
比如添加會(huì)員操作,,由于用于演示,,我只考慮用戶的一些簡(jiǎn)單數(shù)據(jù),當(dāng)后臺(tái)管理員點(diǎn)擊添加用戶時(shí),,只需要傳過來(lái)用戶的姓名和年齡就可以了,,后端接受到數(shù)據(jù)后,將添加創(chuàng)建時(shí)間和更新時(shí)間和默認(rèn)密碼三個(gè)字段,然后保存數(shù)據(jù)庫(kù),。
- @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);
- }
- }
我們只關(guān)注一下上述代碼中的轉(zhuǎn)化代碼,,其他內(nèi)容請(qǐng)忽略:- User user = new User();
- user.setUsername(userInputDTO.getUsername());
- user.setAge(userInputDTO.getAge());
請(qǐng)使用工具
上邊的代碼,從邏輯上講,,是沒有問題的,,只是這種寫法讓我很厭煩,例子中只有兩個(gè)字段,,如果有20個(gè)字段,,我們要如何做呢? 一個(gè)一個(gè)進(jìn)行set數(shù)據(jù)嗎,?當(dāng)然,,如果你這么做了,肯定不會(huì)有什么問題,,但是,,這肯定不是一個(gè)最優(yōu)的做法。
網(wǎng)上有很多工具,,支持淺拷貝或深拷貝的Utils. 舉個(gè)例子,,我們可以使用org.springframework.beans.BeanUtils#copyProperties對(duì)代碼進(jìn)行重構(gòu)和優(yōu)化: - @PostMapping
- public User addUser(UserInputDTO userInputDTO){
- User user = new User();
- BeanUtils.copyProperties(userInputDTO,user);
-
- return userService.addUser(user);
- }
BeanUtils.copyProperties是一個(gè)淺拷貝方法,復(fù)制屬性時(shí),,我們只需要把DTO對(duì)象和要轉(zhuǎn)化的對(duì)象兩個(gè)的屬性值設(shè)置為一樣的名稱,,并且保證一樣的類型就可以了。如果你在做DTO轉(zhuǎn)化的時(shí)候一直使用set進(jìn)行屬性賦值,,那么請(qǐng)嘗試這種方式簡(jiǎn)化代碼,,讓代碼更加清晰!
轉(zhuǎn)化的語(yǔ)義
上邊的轉(zhuǎn)化過程,讀者看后肯定覺得優(yōu)雅很多,,但是我們?cè)賹慾ava代碼時(shí),,更多的需要考慮語(yǔ)義的操作,再看上邊的代碼: - User user = new User();
- BeanUtils.copyProperties(userInputDTO,user);
雖然這段代碼很好的簡(jiǎn)化和優(yōu)化了代碼,,但是他的語(yǔ)義是有問題的,,我們需要提現(xiàn)一個(gè)轉(zhuǎn)化過程才好,所以代碼改成如下:- @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;
- }
這是一個(gè)更好的語(yǔ)義寫法,雖然他麻煩了些,,但是可讀性大大增加了,,在寫代碼時(shí),我們應(yīng)該盡量把語(yǔ)義層次差不多的放到一個(gè)方法中,,比如:- User user = convertFor(userInputDTO);
- return userService.addUser(user);
這兩段代碼都沒有暴露實(shí)現(xiàn),,都是在講如何在同一個(gè)方法中,做一組相同層次的語(yǔ)義操作,,而不是暴露具體的實(shí)現(xiàn),。如上所述,是一種重構(gòu)方式,讀者可以參考Martin Fowler的《Refactoring Imporving the Design of Existing Code》(重構(gòu) 改善既有代碼的設(shè)計(jì)) 這本書中的Extract Method重構(gòu)方式,。
抽象接口定義
當(dāng)實(shí)際工作中,,完成了幾個(gè)api的DTO轉(zhuǎn)化時(shí),我們會(huì)發(fā)現(xiàn),,這樣的操作有很多很多,,那么應(yīng)該定義好一個(gè)接口,讓所有這樣的操作都有規(guī)則的進(jìn)行,。
如果接口被定義以后,,那么convertFor這個(gè)方法的語(yǔ)義將產(chǎn)生變化,他將是一個(gè)實(shí)現(xiàn)類,。
看一下抽象后的接口: - public interface DTOConvert<S,T> {
- T convert(S s);
- }
雖然這個(gè)接口很簡(jiǎn)單,,但是這里告訴我們一個(gè)事情,要去使用泛型,,如果你是一個(gè)優(yōu)秀的java程序員,,請(qǐng)為你想做的抽象接口,做好泛型吧,。我們?cè)賮?lái)看接口實(shí)現(xiàn): - public class UserInputDTOConvert implements DTOConvert {
- @Override
- public User convert(UserInputDTO userInputDTO) {
- User user = new User();
- BeanUtils.copyProperties(userInputDTO,user);
- return user;
- }
- }
我們這樣重構(gòu)后,,我們發(fā)現(xiàn)現(xiàn)在的代碼是如此的簡(jiǎn)潔,并且那么的規(guī)范:- @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
如果你是一個(gè)優(yōu)秀的java程序員,,我相信你應(yīng)該和我一樣,,已經(jīng)數(shù)次重復(fù)review過自己的代碼很多次了。
我們?cè)倏催@個(gè)保存用戶的例子,,你將發(fā)現(xiàn),,api中返回值是有些問題的,問題就在于不應(yīng)該直接返回User實(shí)體,,因?yàn)槿绻@樣的話,,就暴露了太多實(shí)體相關(guān)的信息,這樣的返回值是不安全的,,所以我們更應(yīng)該返回一個(gè)DTO對(duì)象,,我們可稱它為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才更健全。不知道在看完這段代碼之后,,讀者有是否發(fā)現(xiàn)還有其他問題的存在,作為一個(gè)優(yōu)秀的java程序員,,請(qǐng)看一下這段我們剛剛抽象完的代碼:
User user = new UserInputDTOConvert().convert(userInputDTO);
你會(huì)發(fā)現(xiàn),,new這樣一個(gè)DTO轉(zhuǎn)化對(duì)象是沒有必要的,而且每一個(gè)轉(zhuǎn)化對(duì)象都是由在遇到DTO轉(zhuǎn)化的時(shí)候才會(huì)出現(xiàn),,那我們應(yīng)該考慮一下,,是否可以將這個(gè)類和DTO進(jìn)行聚合呢,看一下我的聚合結(jié)果:
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中的轉(zhuǎn)化則由:
User user = new UserInputDTOConvert().convert(userInputDTO);
User saveUserResult = userService.addUser(user);
變成了:
User user = userInputDTO.convertToUser();
User saveUserResult = userService.addUser(user);
我們?cè)貲TO對(duì)象中添加了轉(zhuǎn)化的行為,我相信這樣的操作可以讓代碼的可讀性變得更強(qiáng),,并且是符合語(yǔ)義的,。
再查工具類
再來(lái)看DTO內(nèi)部轉(zhuǎn)化的代碼,它實(shí)現(xiàn)了我們自己定義的DTOConvert接口,,但是這樣真的就沒有問題,,不需要再思考了嗎?
我覺得并不是,,對(duì)于Convert這種轉(zhuǎn)化語(yǔ)義來(lái)講,,很多工具類中都有這樣的定義,這中Convert并不是業(yè)務(wù)級(jí)別上的接口定義,,它只是用于普通bean之間轉(zhuǎn)化屬性值的普通意義上的接口定義,,所以我們應(yīng)該更多的去讀其他含有Convert轉(zhuǎn)化語(yǔ)義的代碼。
我仔細(xì)閱讀了一下GUAVA的源碼,,發(fā)現(xiàn)了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可以完成正向轉(zhuǎn)化和逆向轉(zhuǎn)化,繼續(xù)修改我們DTO中轉(zhuǎn)化的這段代碼:- 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;
- }
- }
看了這部分代碼以后,,你可能會(huì)問,,那逆向轉(zhuǎn)化會(huì)有什么用呢?其實(shí)我們有很多小的業(yè)務(wù)需求中,,入?yún)⒑统鰠⑹且粯拥?,那么我們變可以輕松的進(jìn)行轉(zhuǎn)化,我將上邊所提到的UserInputDTO和UserOutputDTO都轉(zhuǎn)成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;
- }
當(dāng)然,,上述只是表明了轉(zhuǎn)化方向的正向或逆向,,很多業(yè)務(wù)需求的出參和入?yún)⒌腄TO對(duì)象是不同的,那么你需要更明顯的告訴程序:逆向是無(wú)法調(diào)用的:- 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("不支持逆向轉(zhuǎn)化方法!");
- }
- }
看一下doBackward方法,,直接拋出了一個(gè)斷言異常,,而不是業(yè)務(wù)異常,這段代碼告訴代碼的調(diào)用者,,這個(gè)方法不是準(zhǔn)你調(diào)用的,,如果你調(diào)用,我就”斷言”你調(diào)用錯(cuò)誤了,。
|