最近在讀《實現領域驅動設計》這本書,對於業務模型有了不少的看法,也知道該怎麼去設計一個系統,下面我經過一個例子,將我以前的代碼進行一個重構操做前端
若是你如今在使用Eclipse,固然不是說Eclipse徹底是落後的,相比於IDEA,內存消耗和搜索方面是一個很是大的亮點,可是建議仍是用IDEA,也就是JetBean出品的那一套,若是你是學生或者畢業不過久的學生,用你的教育郵箱就能夠免費獲得一個專業版的,何樂不爲,至於更多IDEA的好處,能夠Google一下看看java
根據以往程序員觀念,包括我以前代碼,都有這個毛病程序員
以前關於包名,都是用com.xxx.domain來命名,以爲這個是一個領域對象,針對每個數據庫表都創建一個domain來對應,可是實際上不是這樣,Domain是一個領域對象,在實現領域驅動設計中,Domain是一個貧血模型,是沒有行爲的,或者說是沒有實現領域模型的行爲。因此這些對象應該屬於entity對象,而不是領域對象,應該命名爲com.xxx.entity, 固然具體貧血模型和領域對象的區別最好是看看這本書。spring
對於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);
}
}
複製代碼
咱們在看看這裏面,這裏有個小問題,在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("不支持逆向轉化方法!");
}
}
複製代碼
如今咱們寫完了接口,可是可能也存在個問題,就是咱們好像沒有嚴重DTO,看起來好像比較完美,可能你也會存在疑問,好比關於驗證,不管是前端提供限制,仍是權限驗證,這些不都作了嘛,後端還須要什麼驗證。
任何調用我api或者方法的人,好比前端驗證失敗了,或者某些人經過一些特殊的渠道(好比Charles進行抓包),直接將數據傳入到個人api,那我仍然進行正常的業務邏輯處理,那麼就有可能產生髒數據!
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當初用得很早,詳細不少人都在用這個工具,可以省略咱們大量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);
複製代碼
這樣來的話,可讀性強,並且代碼冗餘和代碼量都不大
咱們設計模式有這個模式,咱們先用原生的試試
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;
}
複製代碼