優秀後端都應該具備的開發好習慣

ITPUB社群發表於2022-11-22


前言

畢業五年多,一共待過3家公司,碰到各種各樣的同事,見識過各種各樣的程式碼,有優雅的,賞心悅目的,也有垃圾的,屎山一樣的。因此,寫這篇文章,來記錄一下一個優秀的後端開發程式設計師,應該有哪些好的開發習慣。

1.註釋儘可能全面,寫有意義的註釋

介面方法、類、複雜的業務邏輯,都應該新增有意義的註釋

  • 對於介面方法的註釋,應該包含詳細的入參和結果說明,有異常丟擲的情況也要詳細敘述
  • 類的註釋應該包含類的功能說明、作者和修改者。
  • 如果是業務邏輯很複雜的程式碼,真的非常有必要寫清楚註釋。

清楚的註釋,更有利於後面的維護。

2.專案拆分合理的目錄結構

記得讀大學那會,剛學做各種各樣的管理系統,都是用MVC模式,也就是controller、service、mapper、entity。如果未來業務擴充套件,你沒有拆分業務結構的話,很可能就會發現,一個service包下,有上百個服務。。。

正確的做法,如果服務過多,應該根據不同的業務進行劃分,比如訂單、登陸、積分等等

優秀後端都應該具備的開發好習慣

當然,你也可以根據不同的業務劃分模組,比如建一個moudles包,然後按訂單、登陸等業務劃分,每個業務都有自己的controller、service、mapper、entity

我們拆分的目的,就是讓專案結構更清晰,可讀性更強,更容易維護而已。

3. 不在迴圈裡遠端呼叫、或者資料庫操作,優先考慮批次進行。

遠端操作或者資料庫操作都是比較耗網路、IO資源的,所以儘量不在迴圈裡遠端呼叫、不在迴圈裡運算元據庫,能批次一次性查回來儘量不要迴圈多次去查。(但是呢,如果是運算元據庫,也不要一次性查太多資料哈,可以分批500一次醬紫)。

正例:

remoteBatchQuery(param);

反例:

for(int i=0;i<n;i++){
  remoteSingleQuery(param)
}

4. 封裝方法形參

如果你的方法引數過多,要封裝一個物件出來。反例如下:

public void getUserInfo(String name,String age,String sex,String mobile,String idNo){
  // do something ...
}

如果引數很多,做新老介面相容處理也比較麻煩。建議寫個物件出來,如下:

public void getUserInfo(UserInfoParamDTO userInfoParamDTO){
  // do something ...
}

class UserInfoParamDTO{
  private String name;
  private String age; 
  private String sex;
  private String mobile;
  private String idNo;
}

5. 封裝通用模板

一個優秀的後端開發,應該具備封裝通用模板的編碼能力。

我們來看一個業務需求:假設我們有這麼一個業務場景:內部系統不同商戶,呼叫我們系統介面,去跟外部第三方系統互動(http方式)。走類似這麼一個流程,如下:

優秀後端都應該具備的開發好習慣

一個請求都會經歷這幾個流程:

  • 查詢商戶資訊
  • 對請求報文加簽
  • 傳送http請求出去
  • 對返回的報文驗籤

透過HTTP發請求出去時,有的商戶可能是走代理的,有的是走直連。假設當前有A,B商戶接入,不少夥伴可能這麼實現,虛擬碼如下:


// 商戶A處理控制程式碼
CompanyAHandler implements RequestHandler {
   Resp hander(req){
   //查詢商戶資訊
   queryMerchantInfo();
   //加簽
   signature();
   //http請求(A商戶假設走的是代理)
   httpRequestbyProxy()
   //驗籤
   verify();
   }
}
// 商戶B處理控制程式碼
CompanyBHandler implements RequestHandler {
   Resp hander(Rreq){
   //查詢商戶資訊
   queryMerchantInfo();
   //加簽
   signature();
   // http請求(B商戶不走代理,直連)
   httpRequestbyDirect();
   // 驗籤
   verify(); 
   }
}

假設新加一個C商戶接入,你需要再實現一套這樣的程式碼。顯然,這樣程式碼就重複了。這時候我們可以封裝一個通用模板!我們就可以定義一個抽象類,包含請求流程的幾個方法,虛擬碼如下:


abstract class AbstractMerchantService  { 

     //模板方法流程
     Resp handlerTempPlate(req){
           //查詢商戶資訊
           queryMerchantInfo();
           //加簽
           signature();
           //http 請求
           httpRequest();
           // 驗籤
           verifySinature();
     }
      // Http是否走代理(提供給子類實現)
      abstract boolean isRequestByProxy();
}

然後所有商戶接入,都做這個流程。如果這個通用模板是你抽取的,別的小夥伴接到開發任務,都是接入你的模板,是不是會有點自豪呀,哈哈~

封裝通用模板,就是抽個模板模式嘛?其實不僅僅是,而是自己對需求、程式碼的思考與總結,一種程式設計思想的昇華

6. 封裝複雜的邏輯判斷條件

我們來看下這段程式碼:

    public void test(UserStatus userStatus){
        if (userStatus != UserStatus.BANNED && userStatus != UserStatus.DELETED && userStatus != UserStatus.FROZEN) {
            //doSomeThing
            return
        }
    }

這段程式碼有什麼問題呢?是的,邏輯判斷條件太複雜啦,我們可以封裝一下它。如下:

    public void test(UserStatus userStatus){
        if (isUserActive(userStatus)) {
            //doSomeThing
        }
    }

    private boolean isUserActive(UserStatus userStatus) {
        return userStatus != UserStatus.BANNED && userStatus != UserStatus.DELETED && userStatus != UserStatus.FROZEN;
    }

7. 保持最佳化效能的嗅覺

優秀的後端開發,應該保持最佳化效能的嗅覺。比如避免建立比必要的物件、非同步處理、使用緩衝流,減少IO操作等等。

比如,我們設計一個APP首頁的介面,它需要查使用者資訊、需要查banner資訊、需要查彈窗資訊等等。假設耗時如下:

優秀後端都應該具備的開發好習慣

查使用者資訊200ms,查banner資訊100ms、查彈窗資訊50ms,那一共就耗時350ms了。如果還查其他資訊,那耗時就更大了。如何最佳化它呢?可以並行發起,耗時可以降為200ms。如下:

優秀後端都應該具備的開發好習慣

之前我寫過一篇後端思維的文章,手把手教大家如何抽並行呼叫框架,大家可以看下:後端思維篇:手把手教你寫一個並行呼叫模板

8. 可變引數的配置化處理

日常開發中,我們經常會遇到一些可變引數,比如使用者多少天沒登入登出運營活動,不同節日紅包皮膚切換、訂單多久沒付款就刪除等等。對於這些可變的引數,不用該直接寫死在程式碼。優秀的後端,要做配置化處理,你可以把這些可變引數,放到資料庫一個配置表裡面,也可以放到專案的配置檔案或者apollo上。

比如產品經理提了個紅包需求,聖誕節的時候,紅包皮膚為聖誕節相關的,春節的時候,為春節紅包皮膚等。如果在程式碼寫死控制,可有類似以下程式碼:

if(duringChristmas){
   img = redPacketChristmasSkin;
}else if(duringSpringFestival){
   img =  redSpringFestivalSkin;
}

如果到了元宵節的時候,運營小姐姐突然又有想法,紅包皮膚換成燈籠相關的,這時候,是不是要去修改程式碼了,重新發布了?

從一開始介面設計時,可以實現一張紅包皮膚的配置表,將紅包皮膚做成配置化呢?更換紅包皮膚,只需修改一下表資料就好了。當然,還有一些場景適合一些配置化的引數:一個分頁多少數量控制、某個搶紅包多久時間過期這些,都可以搞到引數配置化表裡面。這也是擴充套件性思想的一種體現。

9. 會總結並使用工具類。

很多小夥伴,判斷一個list是否為空,會這麼寫:

if (list == null || list.size() == 0) {
  return null;
}

這樣寫呢,邏輯是沒什麼問題的。但是更建議用工具類,比如:

if (CollectionUtils.isEmpty(list)) {
   return null;
}

日常開發中,我們既要會用工具類,更要學會自己去總結工具類。比如去檔案處理工具類、日期處理工具類等等。這些都是優秀後端開發的一些好習慣。

10. 控制方法函式複雜度

你的方法不要寫得太複雜,邏輯不要混亂,也不要太長。一個函式不能超過80行。寫程式碼不僅僅是能跑就行,而是為了以後更好的維護。

反例如下:

public class Test {
    private String name;
    private Vector<Order> orders = new Vector<Order>();

    public void printOwing() {
        //print banner
        System.out.println("****************");
        System.out.println("*****customer Owes *****");
        System.out.println("****************");

        //calculate totalAmount
        Enumeration env = orders.elements();
        double totalAmount = 0.0;
        while (env.hasMoreElements()) {
            Order order = (Order) env.nextElement();
            totalAmount += order.getAmout();
        }

        //print details
        System.out.println("name:" + name);
        System.out.println("amount:" + totalAmount);
        ......
    }
}

其實可以使用Extract Method,抽取功能單一的程式碼段,組成命名清晰的小函式,去解決長函式問題,正例如下:

public class Test {
    private String name;
    private Vector<Order> orders = new Vector<Order>();

    public void printOwing() {

        //print banner
        printBanner();
        //calculate totalAmount
        double totalAmount = getTotalAmount();
        //print details
        printDetail(totalAmount);
    }

    void printBanner(){
        System.out.println("****************");
        System.out.println("*****customer Owes *****");
        System.out.println("****************");
    }

    double getTotalAmount(){
        Enumeration env = orders.elements();
        double totalAmount = 0.0;
        while (env.hasMoreElements()) {
            Order order = (Order) env.nextElement();
            totalAmount += order.getAmout();
        }
        return totalAmount;
    }

    void printDetail(double totalAmount){
        System.out.println("name:" + name);
        System.out.println("amount:" + totalAmount);
    }   
}

11. 在finally塊中對資源進行釋放

應該大家都有過這樣的經歷,windows系統桌面如果開啟太多檔案或者系統軟體,就會覺得電腦很卡。當然,我們linux伺服器也一樣,平時操作檔案,或者資料庫連線,IO資源流如果沒關閉,那麼這個IO資源就會被它佔著,這樣別人就沒有辦法用了,這就造成資源浪費。

我們操作完檔案資源,需要在在finally塊中對資源進行釋放。

FileInputStream fdIn = null;
try {
    fdIn = new FileInputStream(new File("/公眾號_撿田螺的小男孩.txt"));
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}finally {
    try {
        if (fdIn != null) {
            fdIn.close();
        }
    } catch (IOException e) {
        log.error(e);
    }
}

12.把日誌列印好

日常開發中,一定需要把日誌列印好。比如:你實現轉賬業務,轉個幾百萬,然後轉失敗了,接著客戶投訴,然後你還沒有列印到日誌,想想那種水深火熱的困境下,你卻毫無辦法。。。

一般情況,方法入參、出參需要列印日誌,異常的時候,也要列印日誌等等,如下:

public void transfer(TransferDTO transferDTO){
    log.info("invoke tranfer begin");
    //列印入參
    log.info("invoke tranfer,paramters:{}",transferDTO);
    try {
      res=  transferService.transfer(transferDTO);
    }catch(Exception e){
     log.error("transfer fail,account:{}",
     transferDTO.getAccount())
     log.error("transfer fail,exception:{}",e);
    }
    log.info("invoke tranfer end");
    }

之前寫過一篇列印日誌的15個建議,大家可以看看哈:工作總結!日誌列印的15個建議

13. 考慮異常,處理好異常

優秀的後端開發,應當考慮到異常,並做好異常處理。田螺哥給大家提了10個異常處理的建議:

  • 儘量不要使用e.printStackTrace(),而是使用log列印。因為e.printStackTrace()語句可能會導致記憶體佔滿。
  • catch住異常時,建議列印出具體的exception,利於更好定位問題
  • 不要用一個Exception捕捉所有可能的異常
  • 記得使用finally關閉流資源或者直接使用try-with-resource
  • 捕獲異常與丟擲異常必須是完全匹配,或者捕獲異常是拋異常的父類
  • 捕獲到的異常,不能忽略它,至少打點日誌吧
  • 注意異常對你的程式碼層次結構的侵染
  • 自定義封裝異常,不要丟棄原始異常的資訊Throwable cause
  • 執行時異常RuntimeException ,不應該透過catch的方式來處理,而是先預檢查,比如:NullPointerException處理
  • 注意異常匹配的順序,優先捕獲具體的異常

14. 考慮系統、介面的相容性

優秀的後端開發,會考慮系統、介面的相容性。

如果修改了對外舊介面,但是卻不做相容。這個問題可能比較嚴重,甚至會直接導致系統發版失敗的。新手程式設計師很容易犯這個錯誤哦~

因此,如果你的需求是在原來介面上修改,尤其這個介面是對外提供服務的話,一定要考慮介面相容。舉個例子吧,比如dubbo介面,原本是隻接收A,B引數,現在你加了一個引數C,就可以考慮這樣處理:

//老介面
void oldService(A,B){
  //相容新介面,傳個null代替C
  newService(A,B,null);
}

//新介面,暫時不能刪掉老介面,需要做相容。
void newService(A,B,C){
  ...
}

15. 採取措施避免執行時錯誤

優秀的後端開發,應該在編寫程式碼階段,就採取措施,避免執行時錯誤,如陣列邊界溢位,被零整除,空指標等執行時錯誤。類似程式碼比較常見:

String name = list.get(1).getName(); //list可能越界,因為不一定有2個元素哈

所以,應該採取措施,預防一下陣列邊界溢位,正例如下:

if(CollectionsUtil.isNotEmpty(list)&& list.size()>1){
  String name = list.get(1).getName(); 
}



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2924537/,如需轉載,請註明出處,否則將追究法律責任。

相關文章