爲什麼開發效率低,可能是項目結構有問題!

最近做了一個前後端分離高併發的秒殺書城 ,對項目的代碼結構有了新的認識。具體的後臺代碼實踐在這裏。對於這個項目,我總結了四點比較重要的項目結構要點,希望對小夥伴們以後的開發中有新的啓發。

1. 一定要有返回類型

如今較大型的項目都會用到前後端分離的技術,此時,接口和數據的定義就會顯得尤爲重要。爲了給前端返回統一的用戶數據,在一般情況下,我們會爲返回值定義一個實體類,其中的屬性包括返回碼返回描述返回實體,一個例子如下:

{
    "code":200,
    "msg":"ok",
    "data":{
        "this is a response data"
    }
}
  • 我們在代碼中可以這樣編寫

    public class ReturnType {
        /**
         * 狀態碼
         */
        private int code;
        /**
         * 表明對應請求的返回處理結果 "ok" 或 "fail"
         */
        private String status;
    
        /**
         * 若status=success,則data內返回前端需要的json數據
         * 若status=fail,則data內使用通用的錯誤碼格式
         */
        private Object data;
    
        /**
         * 定義一個通用的創建方法,用於返回值爲空的情況
         */
        public static ReturnType create() {
            return ReturnType.create("ok"200);
        }
    
        public static ReturnType create(Object result,int code){
            return ReturnType.create(result,"ok",200);
        }
    
        public static ReturnType create(Object result,String status,int code){
            ReturnType type = new ReturnType();
            type.setStatus(status);
            type.setData(result);
            type.setCode(code);
            return type;
        }
    
        public int getCode() {
            return code;
        }
    
        public int setData(Object code) {
            this.data = code;
        }
        
        public String getStatus() {
            return status;
        }
    
        public void setStatus(String status) {
            this.status = status;
        }
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
    }
    

2. 異常拋出的姿勢

有過面向對象基礎的人都知道,異常是面向對象必不可少的一部分,就Java來說,有ERRORException兩大類,由於本片文章主要說的是異常拋出的一些技巧,所以這些基礎性的知識可以看這篇文章:Java中的異常

在做Java開發的時候,常常需要我們拋出異常。尤其是前後端分離的項目,在正常情況下,前臺給後臺發送請求,後臺通過處理再把數據返回給前臺。但在非正常情況下,譬如庫存不夠,插入的主鍵已經存在等異常情況下,我們必須要把這種情況告訴前臺,這就用到了我們的自定義異常。

舉個例子,如果是庫存不夠的異常,我應該給前臺這樣返回:

{
    "code":502,
    "msg":"apple的庫存不夠",
    "data":{
        "this response shouldn't have response"
    }
}

但是有一個問題,我們如何才能給前臺拋出這個優雅的異常呢?

可以用裝飾者模式解決這個問題

  • 首先定義一個異常接口,作爲抽象組件

    // 裝飾者模式中的抽象組件
    public interface ReturnException {
        /**
         * 得到錯誤代碼
         * @return ErrCode
         */
        int getErrCode();
    
        /**
         * 得到錯誤信息
         * @return ErrMsg
         */
        String getErrMsg();
    
        /**
         * 設置錯誤信息
         * @param errMsg 錯誤信息
         */
        void setErrMsg(String errMsg);
    }
    
  • 然後再定義一個異常實現類,作爲具體裝飾者

    // 具體裝飾者
    public class ReturnExceptionImpl extends Exception implements ReturnException {
    
        private ReturnException returnException;
    
        /**
         * 直接接收EmException的傳參用於構造業務異常
         * @param returnPtin 錯誤類型
         */
        public ReturnExceptionImpl(ReturnException returnException) {
            super();
            this.returnException = returnException;
        }
    
        /**
         * 接收自定義errMsg的方式構造業務異常
         * @param returnException 錯誤類型
         * @param errMsg 錯誤信息
         */
        public ReturnExceptionImpl(ReturnException returnException,String errMsg){
            this.returnException = returnException;
            this.returnError.setErrMsg(errMsg);
        }
    
        @Override
        public int getErrCode() {
            return this.returnError.getErrCode();
        }
    
        @Override
        public String getErrMsg() {
            return this.returnError.getErrMsg();
        }
    
        @Override
        void ReturnException setErrMsg(String errMsg) {
            this.returnException.setErrMsg(errMsg);
        }
    }
    
  • 還有一個枚舉類型,作爲裝飾者中的具體構件

    // 具體構件
    public enum EmException implements ReturnError {
    
        /**
         * 通用錯誤類型 999
         */
        PARAMETER_VALIDATION_ERROR(999, "參數不合法"),
        /**
         * 未知錯誤 888
         */
        UNKNOWN_ERROR(888, "未知異常"),
        /**
         * 10000開頭爲用戶相關信息錯誤定義
          */
        STOCK_NOT_ENOUGH(502,"庫存不夠");
    
        private int errCode;
        private String errMsg;
    
        EmException(int errCode, String errMsg) {
            this.errCode = errCode;
            this.errMsg = errMsg;
        }
    
        @Override
        public int getErrCode() {
            return this.errCode;
        }
    
        @Override
        public String getErrMsg() {
            return this.errMsg;
        }
    
        @Override
        public ReturnException setErrMsg(String errMsg) {
            this.errMsg = errMsg;
            return this;
        }
    }
    
  • 之後,我們碰到異常就可以這樣拋出

     if (!itemService.decreaseStock(orderDTO.getItemId(),orderDTO.getAmount())){
            throw new ReturnExceptionImpl(EmException.STOCK_NOT_ENOUGH);
      }
    
  • 最後一步,我們在SpringBoot中定義全局異常處理,用來接收全局的異常拋出:

    @ControllerAdvice
    public class GlobalExceptionHandler{
        @ExceptionHandler(Exception.class)
        @ResponseBody
        public ReturnType doError(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Exception ex) {
           	String msg;
            int code;
            if( ex instanceof ReturnException){
                ReturnException businessException = (ReturnException)ex;
                code = EmReturnError.UNKNOWN_ERROR.getErrCode();
                msg = businessException.getErrMsg());
            }else if(ex instanceof ServletRequestBindingException){
                code = EmReturnError.UNKNOWN_ERROR.getErrCode();
                msg = "url綁定路由問題";
            }else if(ex instanceof NoHandlerFoundException){
                code = EmReturnError.UNKNOWN_ERROR.getErrCode();
                msg = "沒有找到對應的訪問路徑";
            }else if (ex instanceof IllegalArgumentException) {
                code = EmReturnError.PARAMETER_VALIDATION_ERROR.getErrCode
                msg = "輸入參數不完整";
            }else{
                code = EmReturnError.UNKNOWN_ERROR.getErrCode();
                msg = EmReturnError.UNKNOWN_ERROR.getErrMsg();
            }
            return ReturnType.create(msg,"fail",code);
        }
    }
    

3. 實體類三劍客

所謂的實體類三劍客,即是DO,DTO和VO了。

  • DO,即是DataObject,數據對象。是和數據庫的表一對一的,一般情況下由Mybatis的逆向插件直接生成的
  • DTO,即是DataTransferObject,數據轉換對象。是對DO層的二次抽象,一般前端的數據會填充到DTO中,即Controller的參數實體一般是DTO
  • VO,即是ViewObject,視圖對象。顧名思義,我們後臺返回給前臺的實體即是VO

對於這三者的關係,我舉個例子:大型項目中,用戶其他信息和密碼在數據庫中分開存放的,因爲在實際業務中,密碼和用戶其他信息的讀取和修改概率是不一樣的。在這種業務場景下,DO就會把用戶其他信息和密碼存成兩個實體類,這是和數據庫一一對應的。但是DTO就會把用戶其他信息和密碼合成一個實體類,因爲在業務邏輯中,他們的意義一樣重要。當我們把這些數據返回時,前端可能不需要諸如Id一類的數據,我們就會重新把他們封裝爲VO實體類

以我Github中的秒殺項目爲例,如下所示:

在這裏插入圖片描述

把他們抽象起來來說,就像下圖一樣:

在這裏插入圖片描述

此時,又有一個問題誕生了,DO,DTO和VO怎麼轉化呢?

一般情況下,我們會在它們的所在的層級進行轉化:譬如,我們在Service層中把DTO和DO進行相互轉化,在Controller層中把DTO和VO進行相互轉化。在此我們一定要注意不要手動轉化,太過麻煩,我們要善於利用API,Spring給我們提供了BeanUtils.copyProperties()方法,十分方便快捷。如下:

private CategoryDTO convertDtoFromDO(CategoryInfoDO categoryInfoDO, List<ItemCategoryDO> itemCategoryDOList) {
        CategoryDTO categoryDTO = new CategoryDTO();
        BeanUtils.copyProperties(categoryInfoDO,categoryDTO);
        if (itemCategoryDOList != null) {
            categoryDTO.setItemIds(itemCategoryDOList.stream()
                    .map(ItemCategoryDO::getItemId).collect(Collectors.toList()));
        }
        return categoryDTO;
    }

更近一步去思考,如此多的convertxxx,我們有沒有可能去簡化一下,當然可以!我們可以定義一個反型接口把這些都抽象出來,但是這個就是後話了。

同時,我們還要利用好工具,譬如Lombok。它是通過註解的形式來減輕我們編程的重複勞動。如下

@Data
public class UserDTO() {
    private int id;
    private String name;
    private String pwd;
}

最明顯的一個功能就是它可以幫助我們省略實體類中的getter和setter方法,甚至是toString和hashCode,equals等等各種方便的操作。雖然IDEA可以自動生成這些方法,但是IDEA絕對沒有它的註解方便省事!

4. 項目包的命名

這個不是絕對的,包括上面三種都不是絕對的。

在我一年多的項目開發過程中,結合我自己和大牛項目結構的包名,我覺得有基本的命名和機構有以下這麼幾種:

  • config:這個是一些配置,譬如Redis的配置,Druid的配置等等
  • constant:這個會放置常量類,如枚舉,常量接口等等
  • controller:這個是控制層。有時候VO實體類所在的vo包也在該包下
  • dao:這個是用來操作數據庫的包。mybaits中的mapper也可以存放在這裏面
  • entity:這個包存放DO實體類
  • log:存放日誌相關操作
  • service:這個是服務層。有時候會把DTO實體類所在的model包也放在該包下
  • util:這個就是防止工具類了,一般情況下,工具類的方法都是靜態的,同時該工具類的構造方法也是私有的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章