1. 程式人生 > >寫程式碼有這些想法,同事才不會認為你是複製貼上程式設計師

寫程式碼有這些想法,同事才不會認為你是複製貼上程式設計師

前言

最近做完12月份版本需求,有一些思考不夠深入的程式碼,因此寫一下總結,希望大家日常寫程式碼多點思考,多點總結,加油!同時哪裡有不對的,也望指出。

一、複雜的邏輯條件,是否可以調整順序,讓程式更高效呢。

假設業務需求是這樣:會員,第一次登陸時,需要發一條感謝簡訊。如果沒有經過思考,程式碼直接這樣寫了

if(isUserVip && isFirstLogin){
    sendMsg();
}

假設總共有5個請求,isUserVip通過的有3個請求,isFirstLogin通過的有1個請求。
那麼以上程式碼,isUserVip執行的次數為5次,isFirstLogin執行的次數也是3次,如下:

如果調整一下isUserVip和isFirstLogin的順序呢?

if(isFirstLogin && isUserVip ){
    sendMsg();
}

isFirstLogin執行的次數是5次,isUserVip執行的次數是1次,如下:

醬紫你的程式是否更高效呢?

二、你的程式是否不經意間建立了不必要的物件。

舉個粟子吧,判斷使用者會員是否處於有效期,通常有以下類似程式碼:

//判斷使用者會員是否在有效期
public boolean isUserVIPValid() {
  Date now = new Date();
  Calendar gmtCal = Calendar.getInstance();
  gmtCal.set(2019, Calendar.JANUARY, 1, 0, 0, 0);
  Date beginTime = gmtCal.getTime();
  gmtCal.set(2020, Calendar.JANUARY, 1, 0, 0, 0);
  Date endTime= gmtCal.getTime();
  return now.compareTo(beginTime) >= 0 && now.compareTo(endTime) <= 0;
}

但是呢,每次呼叫isUserVIPValid方法,都會建立Calendar和Date物件。其實吧,除了New Date,其他物件都是不變的,我們可以抽出全域性變數,避免建立了不必要的物件,從而提高程式效率,如下:

public class Test {

    private static final Date BEGIN_TIME;
    private static final Date END_TIME;
    static {
        Calendar gmtCal = Calendar.getInstance();
        gmtCal.set(2019, Calendar.JANUARY, 1, 0, 0, 0);
        BEGIN_TIME = gmtCal.getTime();
        gmtCal.set(2020, Calendar.JANUARY, 1, 0, 0, 0);
        END_TIME = gmtCal.getTime();
    }

    //判斷使用者會員是否在有效期
    public boolean isUserVIPValid() {
        Date now = new Date();
        return now.compareTo(BEGIN_TIME) >= 0 && now.compareTo(END_TIME) <= 0;
    }
}

三、查詢資料庫時,你有沒有查多了資料?

大家都知道,查庫是比較耗時的操作,尤其資料量大的時候。所以,查詢DB時,我們取所需就好,沒有必要大包大攬。

假設業務場景是這樣:查詢某個使用者是否是會員。曾經看過實現程式碼是這樣。。。

List<Long> userIds = sqlMap.queryList("select userId from user where vip=1");
boolean isVip = userIds.contains(userId);

為什麼先把所有會有查出來,再判斷是否包含這個useId,來確定useId是否是會員呢?直接把userId傳進sql,它不香嗎?如下:

Long userId = sqlMap.queryObject("select userId from user where userId='userId' and vip='1' ")
boolean isVip = userId!=null;

實際上,我們除了把查詢條件都傳過去,避免資料庫查多餘的資料回來,還可以通過select 具體欄位代替select *,從而使程式更高效。

四、加了一行通知類的程式碼,總不能影響到主要流程吧。

假設業務流程這樣:需要在使用者登陸時,添加個簡訊通知它的粉絲。
很容易想到的實現流程如下:

假設提供sendMsgNotify服務的系統掛了,或者呼叫sendMsgNotify失敗了,那麼使用者登陸就失敗了。。。

一個通知功能導致了登陸主流程不可用,明顯的撿了芝麻丟西瓜。那麼有沒有魚魚熊掌兼得的方法呢?有的,給發簡訊介面捕獲異常處理,或者另開執行緒非同步處理,如下:

因此,我們新增通知類等不是非主要,可降級的介面時,應該靜下心來考慮是否會影響主要流程,思考怎麼處理最好。

五、對空指標保持嗅覺,如使用equals比較時,常量或確定值放左邊。

NullPointException在Java世界早已司空見慣,我們在寫程式碼時,可以三思而後寫,儘量避免低階的空指標問題。

比如有以下業務場景,判斷使用者是否是會員,經常可見如下程式碼:

boolean isVip = user.getUserFlag().equals("1");

如果讓這個行程式碼上生產環境,待君驀然回首,可能那空指標bug,就在燈火闌珊處。顯然,這樣可能會產生空指標異常,因為user.getUserFlag()可能是null。

怎樣避免空指標問題呢?把常量1放到左邊就可以啦,如下:

boolean isVip = "1".equals(user.getUserFlag());

六、你的關鍵業務程式碼是否有日誌保駕護航?

關鍵業務程式碼無論身處何地,都應該有足夠的日誌保駕護航。

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

那麼,你的轉賬業務都需要那些日誌資訊呢?至少,方法呼叫前,入參需要列印需要吧,介面呼叫後,需要捕獲一下異常吧,同時列印異常相關日誌吧,如下:

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,cifno:{},account:{}",transferDTO.getCifno(),
     transferDTO.getaccount())
     log.error("transfer fail,exception:{}",e);
    }
    log.info("invoke tranfer end");
    }

除了列印足夠的日誌,我們還需要注意一點是,日誌級別別混淆使用,別本該列印info的日誌,你卻列印成error級別,告警半夜三更催你起來排查問題就不好了。

七、對於行數比較多的函式,是否可以劃分小函式來優化呢?

我們在維護老程式碼的時候,經常會見到一坨坨的程式碼,有些函式幾百行甚至上千行,閱讀起來比較吃力。

假設現在有以下程式碼

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);
    }
}

劃分為功能單一的小函式後:

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);
    }
    
}

一個過於冗長的函式或者一段需要註釋才能讓人理解用途的程式碼,可以考慮把它切分成一個功能明確的函式單元,並定義清晰簡短的函式名,這樣會讓程式碼變得更加優雅。

八、某些可變因素,如紅包面板等等,做成配置化是否會更好呢。

假如產品提了個紅包需求,聖誕節的時候,紅包面板為聖誕節相關的,春節的時候,紅包面板等。

如果在程式碼寫死控制,可有類似以下程式碼:

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

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

從一開始,實現一張紅包面板的配置表,將紅包面板做成配置化呢?更換紅包面板,只需修改一下表資料就好了。

九、多餘的import 類,區域性變數,沒引用是不是應該刪除

如果看到程式碼存在沒使用的import 類,沒被使用到的區域性變數等,就刪掉吧,如下這些:

這些沒被引用的區域性變數,如果沒被使用到,就刪掉吧,它又不是陳年的女兒紅,留著會越發醇香。它還是會一起被編譯的,就是說它還是耗著資源的呢。

十、查詢大表時,是否加了索引,你的sql走了索引嘛。

查詢資料量比較大的表時,我們需要確認三點:

  • 你的表是否建了索引
  • 你的查詢sql是否命中索引
  • 你的sql是否還有優化餘地

一般情況下,資料量超過10萬的表,就要考慮給表加索引了。哪些情況下,索引會失效呢?like萬用字元、索引列運算等會導致索引失效。有興趣的朋友可以看一下我這篇文章。
後端程式設計師必備:索引失效的十大雜症

十一、你的方法到底應該返回空集合還是 null呢?

如果返回null,呼叫方在忘記檢測的時候,可能會丟擲空指標異常。返回一個空集合呢,就省去該問題了。

mybatis查詢的時候,如果返回一個集合,結果為空時也會返回一個空集合,而不是null。

正例

public static List<UserResult> getUserResultList(){
    return Collections.EMPTY_LIST;
}

十二、初始化集合時儘量指定其大小

阿里開發手冊推薦了這一點

假設你的map要儲存的元素個數是15個左右,最優寫法如下

 //initialCapacity = 15/0.75+1=21
 Map map = new HashMap(21);

十三、查詢資料庫時,如果資料返回過多,考慮分批進行。

假設你的訂單表有10萬資料要更新狀態,不能一次性查詢所有未更新的訂單,要分批。

反例:

List<Order> list = sqlMap.queryList("select * from Order where status='0'");
for(Order order:list){
  order.setStatus(1);
  sqlMap.update(order);  
}

正例:

Integer count = sqlMap.queryCount(select count(1) from Order where status ='0');
while(true){
    int size=sqlMap.batchUpdate(params);
    if(size<500){
        break;
    }
}

十四、你的介面是否考慮到冪等性,併發情況呢?

冪等性是什麼?
一次和多次請求某一個資源對於資源本身應該具有同樣的結果。就是說,其任意多次執行對資源本身所產生的影響均與一次執行的影響相同。

為什麼需要冪等性?

  • 使用者在APP上連續點選了多次提交訂單,總不能生成多個訂單吧
  • 使用者因為網路卡了,連續點擊發送訊息,接受者總不能收到重複的同一條訊息吧。

假設有業務場景:

使用者點選下載按鈕,系統開始下載檔案,使用者再次點選下載,會提示檔案正在下載中。

有一部分人會這樣實現:

Integer count = sqlMap.selectCount("select count(1) from excel where state=1");
if(count<=0){
    Excel.setStatus(1);
    updateExcelStatus();
    downLoadExcel();
}else{
    "檔案正在下載中"
}

我們可以看一下,兩個請求過來可能會有什麼問題?

執行流程:

  • 第一步,A查詢沒有下載中的檔案。
  • 第二步,B查詢沒有下載中的檔案。
  • 第三步,A開始下載檔案
  • 第四部,B 開始下載檔案

顯然,這樣有問題,同時兩個檔案在下載了。正確的實現方式呢?

if(updateExcelStatus(1){
    downLoadExcel(); 
}else{
    "檔案正在下載中"
}

十五、用一個私有構造器強化你的工具類,此不美哉?

工具類的方法都是靜態方法,通過類來直接呼叫即可。但是有些呼叫方可能會先例項化,再用物件去呼叫,而這就不好了。怎麼避免這種情況,讓你的工具類到達可控狀態呢,新增私有構造器

public class StringUtis{
       private StringUtis(){} ///私有構造類,防止意外例項出現
       public static bool validataString(String str){
           
       }
}

十六、基本不變的使用者資料,快取起來,效能是否有所提升呢

假設你的介面需要查詢很多次資料庫,獲取到各中資料,然後再根據這些資料進行各種排序等等操作,這一系列猛如虎的操作下來,介面效能肯定不好。典型應用場景比如:直播列表這些。

那麼,怎麼優化呢?剖析你排序的各部分資料,實時變的資料,繼續查DB,不變的資料,如使用者年齡這些,搞個定時任務,把它們從DB拉取到快取,直接走快取。

因此,這個點的思考就是,在恰當地時機,適當的使用快取。

待補充...

個人公眾號