你是怎麼在項目中作對象轉換的

最近在讀《實現領域驅動設計》這本書,對於業務模型有了不少的看法,也知道該怎麼去設計一個系統,下面我經過一個例子,將我以前的代碼進行一個重構操做前端

前言

若是你如今在使用Eclipse,固然不是說Eclipse徹底是落後的,相比於IDEA,內存消耗和搜索方面是一個很是大的亮點,可是建議仍是用IDEA,也就是JetBean出品的那一套,若是你是學生或者畢業不過久的學生,用你的教育郵箱就能夠免費獲得一個專業版的,何樂不爲,至於更多IDEA的好處,能夠Google一下看看java

代碼重構

規範化

根據以往程序員觀念,包括我以前代碼,都有這個毛病程序員

  • domain包名

以前關於包名,都是用com.xxx.domain來命名,以爲這個是一個領域對象,針對每個數據庫表都創建一個domain來對應,可是實際上不是這樣,Domain是一個領域對象,在實現領域驅動設計中,Domain是一個貧血模型,是沒有行爲的,或者說是沒有實現領域模型的行爲。因此這些對象應該屬於entity對象,而不是領域對象,應該命名爲com.xxx.entity, 固然具體貧血模型和領域對象的區別最好是看看這本書。spring

  • DTO

對於DTO對象,不少人認爲只有在輸入輸出裏面算,或者只能在上層調用對象纔算DTO,可是這種說法不徹底正確,對於DTO其實只要在網絡中傳輸的對象,均可以叫DTO對象,好比RPC調用等等。數據庫

場景描述

如今有一個商品項目,咱們有一個用戶信息表,須要維護,裏面有三個字端:username,Age,Sex後端

@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());
				user.setSex(userInputDTO.getSex)
        return userService.addUser(user);
    }
}
複製代碼

相信不少人都這樣寫的,在Controller收到UserDTO對象,咱們須要在Service層轉換成BO或者Entity對象設計模式

重點就在這一步api

User user = new User();
        user.setUsername(userInputDTO.getUsername());
        user.setAge(userInputDTO.getAge());
				user.setSex(userInputDTO.getSex)
複製代碼

可是就出現個問題,如今三個字端已經夠繁雜了,可是若是20個字端,那代碼冗餘度就很高了,因此這是最不推薦的作法。數組

使用工具類

咱們瞭解到,這個時候拷貝技術就用到了,直接拷貝過來是最方便最優雅的,好比org.springframework.beans.BeanUtils#copyProperties這方法,咱們用這個工具類直接進行拷貝,這裏注意,這個方法是一個淺拷貝方法,咱們優化一下代碼bash

這裏注意,阿里手冊上是不推薦使用Apache的BeanUtils,由於性能問題,可是這是Spring的工具類

@PostMapping
public User addUser(UserInputDTO userInputDTO){
    User user = new User();
    BeanUtils.copyProperties(userInputDTO,user);
    return userService.addUser(user);
}
複製代碼

這樣的話,代碼就精簡多了,只要把user這個entity對象的字段設置和UserInputDTO對象字端同樣就好了,就算再多字端也不怕了。

語義問題

上面代碼看起來精簡了不少,可是是存在語義問題的,由於不具有很好的可讀性,因此咱們最好仍是專門寫在一個方法裏面,實現DTO的轉換,詳細以下

@PostMapping
 public User addUser(UserInputDTO userInputDTO){
         User user = convertFor(userInputDTO);
         return userService.addUser(user);
 }
// 專門實現一個私有方法,來對DTO實現轉換
 private User convertFor(UserInputDTO userInputDTO){
         User user = new User();
         BeanUtils.copyProperties(userInputDTO,user);
         return user;
 }
複製代碼

這裏其實也應該引發咱們的注意,就是咱們寫代碼時候,也要考慮到不要隨便實現一個轉化,可讀性不好,並且改動也是直接在原有地方改,風險很大,例如若是轉換方式變了這裏,你就要修改addUser方法,下面這種方法,直接在ConvertFor改動便可。因此咱們應該將相同語義的代碼放到同一個層次地方,這裏能夠看到,咱們將轉換方法convertFor私有化了,在重構書裏,咱們把這種重構方式叫作Extract Method,如何在同一個方法中,作一組相同層次的語義操做,而不是暴露具體的實現。

抽象接口定義

在實際寫代碼時候,咱們可能須要大量作一個這樣的操做,UserDTO轉換,ItemDTO轉換等等,咱們應該將這個共同操做給抽離出來,這樣全部操做就有規則執行了,這個時候,咱們知道,convertFor這個方法就不能是一個統一方法,由於入參是根據不一樣DTO變的,這個時候咱們就須要用泛型了。咱們定義一個抽象接口。

public interface DTOConvert {
	T convert(S s);
}
複製代碼

如今這個接口實現了,咱們應該將ConvertFor實現類從新實現一遍了

public class UserInputDTOConvert implements DTOConvert {
	@Override
	public User convert(UserInputDTO userInputDTO) {
	User user = new User();
	BeanUtils.copyProperties(userInputDTO,user);
	return user;
	}
}
複製代碼

這樣,在Service層,咱們就將代碼規範了

@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);
    }
}
複製代碼

Code Review

咱們在看看這裏面,這裏有個小問題,在AddUser這裏,直接返回User,暴露信息不少,前文咱們說,既然進去是DTO,出來也是DTO,那麼這裏咱們徹底能夠在規範一點,返回的也是一個DTO對象,沒有必要直接返回一個完整的User對象

@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;
}
複製代碼

咱們在這裏,你會發現,new這樣一個DTO轉化對象是沒有必要的,並且每個轉化對象都是由在遇到DTO轉化的時候纔會出現,那咱們應該考慮一下,是否能夠將這個類和DTO進行聚合呢

User user = new UserInputDTOConvert().convert(userInputDTO);
複製代碼

咱們用的就是這個convert方法,咱們直接將其融合到UserInputDTO裏面

public class UserInputDTO {
    private String username;
    private int age;
    private String sex;
    
    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 String getSex(){
				return sex;
		}

		public void setSex(String sex){
				this.sex = sex;
		}
		
    public User convertToUser(){
        UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
        User convert = userInputDTOConvert.convert(this);
        return convert;
    }

    private static class UserInputDTOConvert implements DTOConvert{
        @Override
        public User convert(UserInputDTO userInputDTO) {
            User user = new User();
            BeanUtils.copyProperties(userInputDTO,user);
            return user;
        }
    }
}
複製代碼

這樣可讀性也很高,咱們的輸入DTO提供了轉換Entity方法

這樣在Service中的轉換

User user = userInputDTO.convertToUser();
User saveUserResult = userService.addUser(user);
複製代碼

再看工具類

咱們上文實現了一個工具類,經過定義一個抽象接口,咱們可以實現轉換,可是這樣轉換是不完美的,不少工具類都是有轉換類的,好比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);
    //其餘略
}
複製代碼

咱們看到,他是實現了兩個抽象方法,doForward 和doBackward方法,也就是咱們說的正向和逆向轉化,咱們能夠仿照寫一下

原來的

public class UserInputDTOConvert implements DTOConvert {
	@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;
         }
 }
複製代碼

可能你以爲這樣寫有麼有必要,可是在大多數系統中,入參和形參都是同樣的,這樣的話,正向轉換和逆向轉化就很方便了

例如咱們將入DTO和出DTO都綜合在一塊兒,組成一個UserDTO,能夠正向轉也能夠逆向轉

public class UserDTO {
    private String username;
    private int age;
		private String sex;
		
    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 String getSex(){
				return sex;
		}
		
		public void setSex(){
				this.sex = sex;
		}
		
    public User convertToUser(){
            UserDTOConvert userDTOConvert = new UserDTOConvert();
            User convert = userDTOConvert.doForward(this);
            return convert;
    }

    public UserDTO convertFor(User user){
            UserDTOConvert userDTOConvert = new UserDTOConvert();
            UserDTO convert = userDTOConvert.doBackward(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;
            }
    }

}

複製代碼

這樣在Service層的代碼就更加精簡了,由於入和出都是同樣的

@PostMapping
 public UserDTO addUser(UserDTO userDTO){
         User user =  userDTO.convertToUser();
         User saveResultUser = userService.addUser(user);
         UserDTO result = userDTO.convertFor(saveResultUser);
         return result;
 }
複製代碼

在特殊狀況下,入和出都不必定是同樣的,因此咱們須要禁用逆向

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("不支持逆向轉化方法!");
         }
 }
複製代碼

bean驗證

如今咱們寫完了接口,可是可能也存在個問題,就是咱們好像沒有嚴重DTO,看起來好像比較完美,可能你也會存在疑問,好比關於驗證,不管是前端提供限制,仍是權限驗證,這些不都作了嘛,後端還須要什麼驗證。

任何調用我api或者方法的人,好比前端驗證失敗了,或者某些人經過一些特殊的渠道(好比Charles進行抓包),直接將數據傳入到個人api,那我仍然進行正常的業務邏輯處理,那麼就有可能產生髒數據!

Jar 303驗證

public class UserDTO {
    @NotNull
    private String username;
    @NotNull
    private int age;
    @NotNull
 		private String sex;
  	// 餘下省略
}
複製代碼

api驗證

@PostMapping
    public UserDTO addUser(@Valid UserDTO userDTO){
            User user =  userDTO.convertToUser();
            User saveResultUser = userService.addUser(user);
            UserDTO result = userDTO.convertFor(saveResultUser);
            return result;
    }
複製代碼

咱們將這個驗證傳到前端,並轉換爲一個API異常

@PostMapping
public UserDTO addUser(@Valid UserDTO userDTO, BindingResult bindingResult){
     checkDTOParams(bindingResult);

     User user =  userDTO.convertToUser();
     User saveResultUser = userService.addUser(user);
     UserDTO result = userDTO.convertFor(saveResultUser);
     return result;
}
private void checkDTOParams(BindingResult bindingResult){
     if(bindingResult.hasErrors()){
             //throw new 帶驗證碼的驗證錯誤異常
     }
}
複製代碼

BindingResult是Spring MVC驗證DTO後的一個結果集,能夠參考spring 官方文檔

lomlock

lomlock當初用得很早,詳細不少人都在用這個工具,可以省略咱們大量getter setter操做

@Setter
@Getter
public class UserDTO {
    @NotNull
    private String username;
    @NotNull
    private int 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{
        @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("不支持逆向轉化方法!");
        }
    }

}
複製代碼

固然若是隻是作這些作操做固然不足以體現lomlock的強大,具體詳細查看文檔

鏈式風格

這在大數據一些框架裏面不少體現,一般一個類有大幾個個方法,並且要重複調用,甚至還有順序

例如賦值操做

User user = new User();
user.setName("fourous");
user.setPassword("12345");
複製代碼

一樣的,若是有20個屬性,這個清單會拉很長

咱們將這個類再優化一下

public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public Student setName(String name) {
        this.name = name;
        return this;
    }

    public int getAge() {
        return age;
    }

    public Student setAge(int age) {
        return this;
    }
}
複製代碼

如今調用變成了

User user = new User();
user.setName("fourous").setPassWord("12345");
複製代碼

好,咱們在用lomlock優化

@Accessors(chain = true)
@Setter
@Getter
public class Student {
    private String name;
    private int age;
}
複製代碼

使用靜態構造方法

咱們以前發現,每次都要new 一個對象,其實咱們能夠用靜態構造方法來簡化一部分,語義也更加優美一點

例如對於數組建立

List list = new ArrayList();
複製代碼

而在GUANA中,是這樣的,提供了一個Lists工具類

Listlist = Lists.newArrayList();
複製代碼

Lists命名是一種約定(俗話說:約定優於配置),它是指Lists是List這個類的一個工具類,那麼使用List的工具類去產生List,這樣的語義是否是要比直接new一個子類來的更直接一些呢,答案是確定

再回過頭來看剛剛的Student,不少時候,咱們去寫Student這個bean的時候,他會有一些必輸字段,好比Student中的name字段,通常處理的方式是將name字段包裝成一個構造方法,只有傳入name這樣的構造方法,才能建立一個Student對象。

這種徹底能夠用lomlock來優化

@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticName = "of")
public class Student {
    @NonNull private String name;
    private int age;
}
複製代碼

這樣建立對象時候就是這樣的

Student student = Student.of("zs");
複製代碼

咱們鏈式調用一次

Student student = Student.of("zs").setAge(24);
複製代碼

這樣來的話,可讀性強,並且代碼冗餘和代碼量都不大

Build模式

咱們設計模式有這個模式,咱們先用原生的試試

public class Student {
    private String name;
    private int age;

    public String getName() {
            return name;
    }

    public void setName(String name) {
            this.name = name;
    }

    public int getAge() {
            return age;
    }

    public void setAge(int age) {
            this.age = age;
    }

    public static Builder builder(){
            return new Builder();
    }
    public static class Builder{
            private String name;
            private int age;
            public Builder name(String name){
                    this.name = name;
                    return this;
            }

            public Builder age(int age){
                    this.age = age;
                    return this;
            }

            public Student build(){
                    Student student = new Student();
                    student.setAge(age);
                    student.setName(name);
                    return student;
            }
    }

}
複製代碼

調用方式

Student student = Student.builder().name("zs").age(24).build();
複製代碼

咱們lomlock優化一下

@Builder
public class Student {
    private String name;
    private int age;
}
複製代碼

調用方式

Student student = Student.builder().name("zs").age(24).build();
複製代碼

代理模式

正如咱們所知的,在程序中調用rest接口是一個常見的行爲動做,若是你和我同樣使用過Spring 的RestTemplate,我相信你會我和同樣,對他拋出的非http狀態碼異常深惡痛絕。

因此咱們考慮將RestTemplate最爲底層包裝器進行包裝器模式的設計:

public abstract class FilterRestTemplate implements RestOperations {
        protected volatile RestTemplate restTemplate;

        protected FilterRestTemplate(RestTemplate restTemplate){
                this.restTemplate = restTemplate;
        }

        //實現RestOperations全部的接口
}
複製代碼

而後再由擴展類對FilterRestTemplate進行包裝擴展:

public class ExtractRestTemplate extends FilterRestTemplate {
    private RestTemplate restTemplate;
    public ExtractRestTemplate(RestTemplate restTemplate) {
            super(restTemplate);
            this.restTemplate = restTemplate;
    }

    public RestResponseDTOpostForEntityWithNoException(String url, Object request, ClassresponseType, Object... uriVariables) throws RestClientException{
            RestResponseDTOrestResponseDTO = new RestResponseDTO();
            ResponseEntitytResponseEntity;
            try {
                    tResponseEntity = restTemplate.postForEntity(url, request, responseType, uriVariables);
                    restResponseDTO.setData(tResponseEntity.getBody());
                    restResponseDTO.setMessage(tResponseEntity.getStatusCode().name());
                    restResponseDTO.setStatusCode(tResponseEntity.getStatusCodeValue());
            }catch (Exception e){
                    restResponseDTO.setStatusCode(RestResponseDTO.UNKNOWN_ERROR);
                    restResponseDTO.setMessage(e.getMessage());
                    restResponseDTO.setData(null);
            }
            return restResponseDTO;
    }
}
複製代碼

包裝器ExtractRestTemplate很完美的更改了異常拋出的行爲,讓程序更具備容錯性。

在這裏咱們不考慮ExtractRestTemplate完成的功能,讓咱們把焦點放在FilterRestTemplate上,「實現RestOperations全部的接口」,這個操做絕對不是一時半會能夠寫完的

public abstract class FilterRestTemplate implements RestOperations {

    protected volatile RestTemplate restTemplate;

    protected FilterRestTemplate(RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
    }

    @Override
    public T getForObject(String url, ClassresponseType, Object... uriVariables) throws RestClientException {
            return restTemplate.getForObject(url,responseType,uriVariables);
    }

    @Override
    public T getForObject(String url, ClassresponseType, MapuriVariables) throws RestClientException {
            return restTemplate.getForObject(url,responseType,uriVariables);
    }

    @Override
    public T getForObject(URI url, ClassresponseType) throws RestClientException {
            return restTemplate.getForObject(url,responseType);
    }

    @Override
    public ResponseEntitygetForEntity(String url, ClassresponseType, Object... uriVariables) throws RestClientException{
            return restTemplate.getForEntity(url,responseType,uriVariables);
    }
    //其餘實現代碼略。。。
}
複製代碼

咱們用lomlock就很簡潔

@AllArgsConstructor
public abstract class FilterRestTemplate implements RestOperations {
    @Delegate
    protected volatile RestTemplate restTemplate;
}
複製代碼
相關文章
相關標籤/搜索