《碼處高效:Java開發手冊》之程式碼風格

小懶程式設計日記發表於2022-04-10

流水淡,碧天長,鴻雁成行。編碼風格,簡捷清爽,反引無限風光。

  在美劇《矽谷》中有這樣一個經典鏡頭,主人公 Richard 與同為開發工程師的女友鬧分手,理由是兩人對縮排方式有著截然不同的程式設計習慣,互相鄙視對方的程式碼風格。Richard 認為" one tab saves four spaces ”,縮排使用 Tab 鍵操作更快,更節省存 儲空間,而女友堅持使用空格縮排,連續四次敲擊空格的聲音,把 Richard 折磨到幾近崩潰,認為這是種精神折磨。 Richard 覺得難以相處,吵完架下樓梯時,不小心摔倒了 還淡定地說,"I just tried to go down the stairs four steps at a time ” (這只是表達我的立場而已)。Tab 鍵和空恪鍵的爭議在現實程式設計中確實存在。除此之外,在其他程式碼風格上,也存在不同的處理方式,往往是誰也說服不了誰,都站在自身“完全正確”的立場上,試圖說服對方。這在團隊開發效率上,往往是一個巨大的內耗,無休止的爭論與最後的收益是成反比的。所以我們認為一致性很重要,就像交通規則一樣,我國規定靠右行駛,有些國家則規定靠左行駛,並沒有絕對的優劣之分,但是在同一個國家或地區內必須要有統一的標準。程式碼風格也是如此,無論選擇哪一種處理方式,都需要部分人犧牲小我,成就大我,切實提升團隊的研發效能。

  程式碼風格並不影響程式執行,沒有潛在的故障風險,通常與資料結構、邏輯表達無關,是指不可見字元的展示方式、程式碼元素的命名方式和程式碼註釋風格等。比如,大括號是否換行、縮排方式、常量與變數的命名方式、註釋是否統一放置在程式碼上方等。程式碼風格的主要訴求是清爽統一、便於閱讀和維護。統一的程式碼風格可以讓開發工程師們沒有嚴重的程式碼心理壁壘,每個人都可以輕鬆地閱讀並快速理解程式碼邏輯,便於高效協作,逐步形成團隊的程式碼“味道”。

命名規約

  程式碼元素包括類、方法、引數、常量、變數等程式中的各種要素。合適的命名,可以體現出元素的特徵、職責 ,以及元素之間的差異性和協同性。為了統一程式碼風格,元素的命名要遵守以下約定。

  1. 命名符合本語言特性

      當前主流的程式語言有 50 種左右,分為兩大陣營—物件導向與程式導向,但是按變數定義和賦值的要求,分為強型別語言和弱型別語言。每種語言都有自己的獨特命名風格,有些語言在定義時提倡以字首來區分區域性變數、全域性變數、控制元件型別。比如 Ii_count 表示 local int 區域性整型變數, dw_report 表示 data window 用於展示報表資料的控制元件。有些語言規定以下畫線為字首來進行命名。這些語言的命名風格,自成一派,也無可厚非,但是在同種語言中,如果使用多種語言的命名風格 就會引起其他開發工程師的反感。比如,在 Java 中,所有程式碼元素的命名均不能以下畫線或美元符號開始或結束。

  2. 命名體現程式碼元素特徵

    ​   命名上可體現出程式碼元素的特徵,僅從名字上即可知道程式碼元素的屬性是什麼,有利於快速理清程式碼脈絡。物件導向程式碼元素的命名形式分為兩大類,即首字母大寫的 UpperCamelCase 和首字母小寫的 lowerCamelCase ,前者俗稱大駝峰,後者俗稱小駝峰。類名採用大駝峰形式,一般為名詞,例如 Object、StringBuffer、 FileInputStream 等。 方法名採用小駝峰形式,一般為動詞,與引數組成動賓結構,例如Object的wait()、StringBuffer的append(String)、FileInputStream的read() 等。變數包括引數、成員變數、區域性變數等,也採用小駝峰形式。常量的命名方式比較特殊,字母全部大寫,單詞之間用下畫線連線。常量和變數是最基本的程式碼元素,就像血液中的紅細胞一樣無處不在。合理的命名有利於保障程式碼機體的清爽、健康。

    ​   在命名時若能體現出元素的特徵,則有助於快速識別命名物件的作用,有助於快速理解程式邏輯。我們推薦在 Java 命名時,以下列方式體現元素特徵:

    • 包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,但是類名如果有複數含義,則可以使用複數形式。
    • 抽象類命名使用 Abstract或Base 開頭,異常類命名使用 Exception 結尾;測試類命名以它要測試的類名開始,以Test 結尾。
    • 型別與中括號緊挨相連來定義陣列。
    • 列舉類名帶上 Enum 字尾,列舉成員名稱需要全大寫,單詞間用下畫線隔開。
  3. 命名最好望文知義

    ​   望文知義是在不需要額外解釋的情況下,僅從名稱上就能夠理解某個詞旬的確切含義。在程式碼元素命名時做到望文知義,從而減少註釋內容,達到自解釋的目的。在實踐中,望文知義的難度是最大的,就好像給孩子起名一樣需要反覆斟酌。文不對題的命名方式,肯定會加大理解成本,更大的罪過是把程式設計師引導到一個錯誤的理解方向上。某些不規範的縮寫會導致理解成本增加。比如 condition 縮寫成 condi 類似隨意的縮寫會嚴重降低程式碼的可理解性。再比如,以單個字母命名的變數,在上下文理解時 會帶來很大的困擾。本書中的所有示例程式碼都比較精筒,沒有具體業務含義。重點在於闡述示例背後的程式設計思維,所以採用單字母的簡潔命名方式,在實際業務程式碼中請勿模仿。

    ​   主流的程式語言基本上以英語為基礎,此處望文知義的“文”指的是英文。隨著開源社群的發展與繁榮,各國程式設計師踴躍參與開源專案的共建,國際交流與合作越來越頻繁,英語能力已經成為程式設計師必備的基礎技能之一。雖然有人認為命名方式應該符合本國語言習慣,拼音這種命名方式,應該是被允許的,但是在國際化專案或開源專案中,對於非漢語國家的開發工程師而言,拼音這種命名方式的可讀性幾乎為零。即使在漢語系家,拼音也存在地區差異。中英文混合的方式,更不應該出現。比如在某業務程式碼中,曾經出現過DaZePromotion ,猜了很久才被命名者告知是打折促銷的類。最讓人無法容忍的是拼音“首字母”簡寫的命名方式,即使發揮極致的想象力,也很難猜出具體的含義,比如 PfmxBuilder 名稱意思是評分模型的建立工廠類!這些命名方式,極大增加了程式的理解成本。所以,正確的英文拼寫和語法可以讓閱讀者易於理解,避免歧義。 alibaba、taobao、hangzhou 等國際通用的名稱,可視同英文。某些複合語義的情況下,儘量使用完整的單片語合來達到望文知義的目的 比如 KeyboardShortcutsHandler、AtomicReferenceFieldUpdater。

    ​   命名要符合語言特性、體現元素特徵。命名做到望文知義、自解釋是每個開發工程師的基本素質之一。我們在思量更好的程式碼元素命名的同時,也要敢於修改已有的、不合理的命名方式。

    ​   在所有程式碼元素中,常量和變數最為常見,優雅地定義與使用好它們,是開發工程師的基本功之一。

常量

​   什麼是常量?常量是在作用域內保持不變的值, 一般用 final 關鍵字進行修飾,根據作用域區分,分為全域性常量、類內常量、區域性常量。全域性常量是指類的公開靜態屬性 使用 public static final 修飾;類內常量是私有靜態屬性,使用 private static final 修飾,區域性常量分為方法常量和引數常量,前者是在方法或程式碼塊內定義的常量,後者是在定義形式引數時 增加 final 表示此引數值不能被修改。全域性常量和類內常量是最主要的常量表現形式,它們的命名方式比較特殊,採用字母全部大寫、單詞之間加下畫線的方式。而區域性常量採用小駝峰形式即可。示例程式碼如下:

public class Constant { 
    public static final String GLOBAL CONSTANT = "shared in global";
    private static final String CLASS CONSTANT = "shared in class";
    
    public void f(String a) { 
        final String methodConstant = "shared in method";
    }
    
    public void g( final int b) {
        // 編譯出錯,不允許對常量引數進行重新賦值
        b = 3;
    }
}

​    常量在程式碼中具有穿透性,使用甚廣。如果沒有一個恰當的命名,就會給程式碼閱讀帶來沉重的負擔,甚至影響對主幹邏輯的理解。首當其衝的問題就是到處使用魔法值。魔法值即“共識層面”上的常量,直接以具體的數值或者字元出現在程式碼中。這些不知所云的魔法值極大地影響了程式碼的可讀性和可維護性。下面先來看一段實際業務程式碼。

public void getOnlinePackageCourse(Long packageId, Long userId) { 
    if (packageId == 3) { 
        logger.error("線下課程,無法線上觀看");
        return;
    }
    // 其它邏輯處理
    PackageCourse online = packageService.getByTeacherId(userId); 
    if (online.getPackageId() == 2) { 
        logger.error("未稽核課程");
        return;
    }
    // 其他邏將處理
}

​   以上示例程式碼中,信手拈來的2和3分別表示未稽核課程和線下課程,僅僅是兩個數字,似乎很容易記憶。但事實上除2和3兩種狀態外,還有1、4、5分別代表新建、稽核未通過、稽核通過。在團隊規模較小時,口口相傳,倒也勉強能夠記住這五個數字的含義,早期還有零星的註釋,駕輕就熟的情況下,連註釋也省了。現實是殘酷的,團隊迅速擴大後,課程狀態個數也在逐步增加,新來的開發工程師在上線新功能模組時,把“稽核通過”和“未稽核課程”對應的數字搞反了,使得課程展示錯誤,導致使用者大量投訴。隨著應用變得越來越複雜,這些魔法值幾乎成了整個後臺服務程式碼中的夢魔。團隊架構師終於下定決心進行系統重構,把這些魔法值以合適的命名方式定義成全域性常量。使用 Enum 列舉類來定義課程型別,示例程式碼如下:

public enum CourseTypeEnurn { 
    /**
     * 允許官方和講師建立和運營
     */ 
    VIDEO_COURSE(l, "錄插課程"),
    /**
     * 只允許官方建立和運營,初始化必須設定合理的報名人數上限
     */
    LIVE_COURSE(2, "直播課程"),
    /**
     * 只允許官方建立和運營
     */
    OFFLINE_COURSE(3,"線下課程");
                
    private int seq; 
    private String desc ; 
                
    CourseTypeEnurn (int seq, String desc) { 
        this.seq = seq; 
        this. desc = desc ; 
    }
                
    public int getSeq() {
        return seq;
    }
                
    public String getDesc() { 
        return desc;
    }
}

​   上述示例程式碼把課程型別分成三種:錄播課程、直播課程、線下課程。列舉型別幾乎是固定不變的全域性常量,使用頻率高、範圍廣,所以列舉常量都需要新增清晰的註釋,比如業務相關資訊或注意事項等。再把課程狀態分為新課程、未稽核課程、稽核通過、稽核未通過、已刪除五種狀態。考慮到後續課程狀態還會再追加,並且狀態沒有擴充套件資訊,所以用不能例項化的抽象類的全域性常量來表示課程狀態,示例程式碼如

下:

public abstract class BaseCourseState { 
    public static final int NEW_COURSE = 1; 
    public static final int UNAUTHED_COURSE = 2;
    public static final int PASSED_COURSE = 3; 
    public static final int NOT_PASSED_COURSE = 4; 
    public static final int DELETED_COURSE = 5;
}

使用重構後的常量修改原有的魔法值,對比一下程式碼的可讀性

public void getOnlinePackageCourse(Long packageId, Long userId) { 
    if (packageId == CourseTypeEnum.OFFLINE_COURSE.getSeq()) { 
        logger.error("線下課程,無法線上觀看");
        return;
    }
    // 其它邏輯處理
    VideoCourse course = packageService.getByTeacherId(userId); 
    if (course.getState() == BaseCourseState.UNAUTHED_COURSE) { 
        logger.error("未稽核課程");
        return;
    }
    // 其他邏將處理
}

​   我們認為,系統成長到某個階段後,重構是種必然選擇。優秀的架構設計不是去阻止未來切重構的可能性,畢竟技術枝、業務方向和規模都在不斷變化,而是儘可能讓重構來得晚一些,重構幅度小一些。即使類內常量和區域性常量當前只使用一次,也需要賦予一個有意義的名稱,目的有兩個:第一、望文知義,方便理解 第二、後期多次使用時能夠保證值出同源。因此,無論如何都不允許任何魔法值直接出現在程式碼中,避免魔法值隨意使用導致取值不一致,特別是對於字串常量來說,應避免沒有預先定義,就直接使用魔法值。所謂常 在河邊走,哪有不溼鞋,在反覆的複製與貼上後,難免會出現問題,警示程式碼如下:

String key = "Id#taobao_" + tradeId;
cache.put(key, value);

​   上述程式碼是儲存資訊到快取中的方法,即使用魔法值組裝 Key。這就導致各個呼叫方到處複製和貼上字串 Id#taobao_ 這樣似乎很合理。但某一天,某個粗心的程式設計師把Id#taobao_ 複製成為Id#taobao,少了下畫線。這個錯誤在測試過程中,並不容易被發現 因為沒有命中快取,會自動訪問資料庫。但在大促時,資料庫壓力急劇上升,進而發現快取全部失效,導致連線佔滿,查詢變慢。小處不小,再次說明魔法值害人害己。

​   某些公認的字面常量是不需要預先定義的,如 for( int i=0; ... )這裡的0是可以直接使用的。true和 false也可以直接使用,但是如果具備了特殊的含義,就必須定義出有意義的常量名稱,比如在 TreeMap 原始碼中,表示紅黑樹節點顏色的 true 和 false 就被定義成為類內常量,以方便理解∶

private static final boolean RED = false;
private static final boolean BLACK = true;

​   常量命名應該全部大寫,單詞間用下畫線隔開,力求語義表達完整清楚,不要嫌名字長,比如,把最大庫存數量命名為 MAX_STOCK_COUNT,把快取失效時間命名為 CACHE_EXPIRED TIME。

變數

​   什麼是變數從廣義來說,在程式中變數是一切通過分配記憶體並賦值的量,分為不可變數(常量)和可變變數。從狹義來說,變數僅指在程式執行過程中可以改變其值的量,包括成員變數和區域性可變變數等。

​ 
  一般情況下,變數的命名需要滿足小駝峰格式,命名體現業務含義即可。存在一種特殊情況,在定義類成員變數時,特別是在 POJO類中,針對布林型別的變數,命名不要加 is 字首,否則部分框架解析會引起序列化錯誤。例如,定義標識是否刪除的成員變數為 Boolean isDeleted,它的 getter 方法也是 isDeleted(),框架在反向解析的時候,"誤以為"對應的屬性名稱是 deleted,導致獲取不到屬性,進而丟擲異常。但是在資料庫建表中,推薦表達是與否的值採用 is_xxx 的命名方式,針對此種情況,需要在<resultMap>中設定,將資料表中的 is_xxx 欄位對映到 POJO類中的屬性Xxx。

程式碼展示風格

縮排、空格與空行

  縮排、空格與空行造就了程式碼的層次性和規律性,有助於直觀、快速、準確地理解業務邏輯。沒有縮排、空格和空行的程式碼可讀性極差。如下反例所示∶

table=newTab;
if (oldTab!=null){ for(int j=0;j<oldCap;++j){if((e=oldTab[j])!=null){
oldTab[j]=null;
if (e.next==null) 
newTab[e.hash&(newCap-1)]=e;else if(e instanceof TreeNode)
if(loTail==null)loHead=e;else oTail.next=e;modCount++;
if((tab=table)!=null&&size>=0){
for(int i=0;i<tab.length;++i)tab[i]=null;
// 其他程式碼
}   
  1. 縮排
      縮排表示層次對應關係。使用 Tab 鍵縮排還是空格縮排長期以來備受爭議,形成兩大陣營。每當在分享會現場調研縮排方式選擇的時候,參與度幾乎都是100%,通常支援空格的人數多於支援Tab 鍵的人數。這時候 Tab 鍵方一般都會提出∶"空格不是有2、4、8個之分嗎?不如讓空格方繼續投票一下,我們Tab 鍵方還是非常團結一致的"。某報告對40萬個開原始碼庫進行了調研,發現近75%的程式碼檔案使用了空格進行縮排。對於團隊協作來說,一致性風格很重要。我們推薦採用4個空格縮排,禁止使用Tab 鍵。

    ​   由於不同編輯器對 Tab 的解析不一致,因此視覺體驗會有差異,而空格在編輯器之間是相容的。2個空格縮排的層次區分度不明顯,超過4個空格的縮排方式又留白過多,且大多數IDE 預設為4個空格縮排,所以我們採用4個空格的縮排方式。對習慣用 Tab 鍵的工程師來說,唯一的福音是很多IDE 工具提供了Tab 鍵與空格之間的快速轉換設定。IDEA 設定 Tab 鍵為4個空格時,請勿勾選 Use tab character;而在Eclipse 中,必須勾選 Insert spaces for tabs。

  2. 空格
      空格用於分隔不同的程式設計元素。空格可以讓運算子、數值、註釋、引數等各種程式設計元素之間錯落有致,方便快速定位。空格的使用有如下約定∶
    (1)任何二目、三目運算子的左右兩邊都必須加一個空格。

    (2)註釋的雙斜線與註釋內容之間有且僅有一個空格。

    (3)方法引數在定義和傳入時,多個引數逗號後邊必須加空格。

    (4)沒有必要增加若干空格使變數的賦值等號與上一行對應位置的等號對齊。

    (5)如果是大括號內為空,則簡潔地寫成{}即可,大括號中間無須換行和空格。

    (6)左右小括號與括號內部的相鄰字元之間不要出現空格。

    (7)左大括號前需要加空格。

    ​   例如,有些工程師習慣在多行賦值語句中對齊等號,如果增加了一條較長的賦值語句,工程師需要更新之前所有的語句對齊格式,這種做法無疑提高了開發成本。此外,雖然不推薦空大括號的程式碼出現,但可能會存在幹某些測試程式碼或者流程語句中,我們推薦空大括號中間無須換行和空格。詳細的示例程式碼如下,重點看註釋內容∶

    public class SpaceCodeStyle {
        // 沒有必要增加若干空格使變數的賦值等號與上一行對應位置的等號對齊
        private static Integer one = 1;
        private static Long two = 2L;
        private static Float three = 3F;
        private static StringBuilder sb = new StringBuilder("code style:");
    
        //縮排 4 個空格(注意∶本程式碼中的任何註釋在雙斜線與註釋內容之間有且僅有一個空格)
        public static void main (String[] args)(
            //繼續縮排4個空格
            try {
            // 任何二目運算子的左右必須有一個空格
                int count = 0;
                // 三目運算子的左右兩邊都必須有一個空格
                boolean condition =(count == 0)? true : false;
                // 關鍵詞if與左側小括號之間必須有一個室格
                // 左括號內的字母c與左括號、字母n與右括號都不需要空格
                 // 右括號與左大括號前加室格且不換行,左大括號後必須換行
                if (condition) {
                    System.out.println ("world");
                    // else 的前後都必須加空格
                    // 右大括號前換行,右大括號後有 else時,不用換行
                } else {
                    System.out.println ("ok");
                    //在右大括號後直接結束,則必須換行
                }
                //如果是大括號內為空,則簡潔地寫成{}即可,大括號中間無須換行和空格
            } catch (Exception e){}
    
            // 在每個實參逗號之後必須有一個空格
            String result = getString(one, two, three, sb);
            System.out.println (result);
        }
        //方法之間,通過空行進行隔斷。在方法定義中,每個形參之後必須有一空格
        private static String getString(Integer one, Long two, Float three, 
                                        StringBuilder sb){
            // 任何二目運算子的左右必須有一個空格,包括賦值運算子,加號運算子等
            Float temp = one + two + three;
            sb.append (temp);
            return sb.toString();
        }
    }
    
  3. 空行
      空行用來分隔功能相似、邏輯內聚、意思相近的程式碼片段,使得程式佈局更加清晰。在瀏覽程式碼時,空行可以起到自然停頓的作用,提升閱讀程式碼的體驗。哪些地方需要空行呢?在方法定義之後、屬性定義與方法之間、不同邏輯、不同語義、不同業務的程式碼之間都需要通過空行來分隔。

換行與高度

  1. 換行

  程式碼中需要限定每行的字元個數,以便適配顯示器的寬度,以及方便CodeReview時進行 diff 比對。對於無節制的行數字符,需要不斷地拉取左右滾動條或者鍵盤移動游標,那是多麼差的體驗。因此,約定單行字元數不超過120個,超出則需要換行,換行時遵循如下原則∶
(1)第二行相對第一行縮排4個空格,從第三行開始,不再繼續縮排,參考示例。

(2)運算子與下文一起換行。

(3)方法呼叫的點符號與下文一起換行。

(4)方法呼叫中的多個引數需要換行時,在逗號後換行。

(5)在括號前不要換行。

StringBuffer sb = new StringBuffer();
// 超過120個字元的情況下,換行縮排 4個空格,並且方法前的點號一起換行
sb.append ("ma").append("chu")...
    .append ("gao")... 
    .append ("xiao")... 
    .append("yealh");
  1. 方法行數限制

    ​   水平方向上對字元數有限制,那麼垂直方向上呢?對於類的長度,只要類功能內聚,不做強制要求。但方法是執行單位,也是閱讀程式碼邏輯的最高粒度模組。龐大的方法容易引起閱讀疲勞,讓人抓不住重點。程式碼邏輯要分主次、個性和共性。不要把不同層次的邏輯寫在一個大方法體裡,應該將次要邏輯抽取為獨立方法,將共性邏輯抽取成為共性方法(比如引數校驗、許可權判斷等),便於複用和維護,使主幹程式碼邏輯更加清晰。

    ​   高內聚、低耦合是程式設計師最熟悉的口號。如何內聚和解耦,其實方法的行數限制就引發了這些維度的思考。把相關的功能強內聚,把弱相關的功能拆解開來,重新抽象、重新封裝。在拆分方法的過程中,通常會糾結對引數的處理,因為拆分的各個方法之間需要通過引數才能傳遞資料。有這種糾結的前提是方法需要傳入大量的引數,事實上這是另外一個話題。限制引數列表過長的方式有很多,比如包裝成類、隱式傳遞或放在集合中等。

    ​   綜上所述,約定單個方法的總行數不超過80行。詳細的判定標準如下,除註釋之外,方法簽名、左右大括號、方法內程式碼、空行、回車及任何不可見字元的總行數不超過80 行。為什麼是80 行?心理學認為人對事物的印象通常不能超過3這個魔法數,三屏是人類短期記憶的極限,而80 行在一般顯示器上是兩屏半的程式碼量。另外,通過對阿里程式碼抽樣調查顯示,只有不到5% 的方法才會超過 80行,而這些方法通常都有明顯的優化空間。

    ​    最後有人說,80行的硬性要求會讓程式設計師在寫程式碼時刻意將多個變數定義在一行,或者if後不寫大括號,或者catch 程式碼後使用空語句{}結束。每個公司都有一些強制的程式碼風格,肯定有些是大家的程式碼素養決定的,少數人偏偏冒天下之大不韙,被這個群體淘汰也是遲早的事情。

  2. 控制語句
      控制語句是底層機器碼跳轉指令的實現。方法內部的跳轉控制主要由條件判斷語句和迴圈語句實現。跳轉能力使程式能夠處理複雜邏輯,具備像人一樣的判斷能力和記憶回溯能力。條件判斷主要由 if、switch、三目運算子組成。迴圈嚴格意義上也是一種跳轉,主要由 for、while、do-while 組成。
    控制語句是最容易出現 Bug 的地方,所以特別需要程式碼風格的約束,而不是天馬行空地亂跳。控制語句必須遵循如下約定∶
    (1)在if、else、for、while、do-while等語句中必須使用大括號。即使只有一行程式碼,也需要加上大括號.

    (2)在條件表示式中不允許有賦值操作,也不允許在判斷表示式中出現複雜的邏輯組合。有些控制語句的表示式邏輯相當複雜,與、或、取反混合運算甚至穿插了賦值操作,理解成本非常高,甚至會產生誤解。要解決這個問題,有一個非常簡單的辦法∶將複雜的邏輯運算賦值給一個具有業務含義的布林變數。例如∶

    // 邏輯判斷中使用複雜的邏輯判斷,不易於理解
    if((file.open(fileName,"w")!= null) && (...) || !(...)){
    }
    言
    //將複雜的邏輯運算賦值給一個易於理解的布林變數,方便閱讀程式碼
    final boolean existed =(file.open(fileName,"w")!= null)
        && (...) || !(...);
    if (existed) {
        ...
    }
    

    (3) 多層巢狀不能超過3層。多層巢狀在哪裡都不受歡迎,是因為條件判斷和分支邏輯數量呈指數關係。如果非得使用多層巢狀,請使用狀態設計模式。對於超過3層的if-else 的邏輯判斷程式碼,可以使用衛語句、策略模式、狀態模式等來實現,其中衛語句示例如下:

    public void today(){
        if(isBusy()){
            System.out.println("change time.");
            return;
        }
        
        if(isFree()){
            System.out.println("go to travel.");
            return;
        }
        
        System.out.println("stay at home to learn Easy Coding.");
        return;
    }
    

    (4)避免採用取反邏輯運算子。取反邏輯不利於快速理解,並且取反邏輯寫法必然存在對應的正向邏輯寫法。比如使用if(x<628)表達x小於628,而不是使用if(!(x >=628))。

程式碼註釋

註釋三要素

  註釋是一個看起來簡單,容易被忽視,但是作用又不容小覷的話題。好的註釋能起到指路明燈、撥雲見日、警示等作用,具體包括∶能夠準確反映設計思想和程式碼邏輯;能夠描述業務含義,使其他工程師能迅速瞭解背景知識。與程式碼不同,註釋沒有語法的限制,完全取決於編寫者的能力和發揮,但這並不意味著註釋可以天馬行空。書寫註釋要滿足優雅註釋三要素。

  1. Nothing is strange

    ​   完全沒有註釋的大段程式碼對於閱讀者來說形同天書。註釋是給自己看的,即使離寫完程式碼很長時間,也能清晰地理解當時的思路;註釋也是給維護者看的,使其能夠快速理解程式碼邏輯。

    ​   相信大多數人閱讀JDK 原始碼時都十分吃力,比如併發控制、集合演算法等,這些天才級的程式基本上沒有任何註釋。JDK的程式碼穩定、高效壓倒一切,不會朝編夕改。但是業務程式碼需要被不斷地維護更新,沒有註釋的程式碼給人一種陌生感。世界上最遙遠的距離是,我和要修改的程式碼間缺少一段註釋。因此,我們提倡要寫註釋,然後才是把註釋寫得精簡。

  2. Less is more

    ​   從程式碼可讀性及維護成本方面來講,程式碼中的註釋一定是精華中的精華。首先,真正好的程式碼是自解釋的,準確的變數命名加上合理的程式碼邏輯,無須過多的文字說明就足以讓其他工程師理解程式碼的功能。如果程式碼需要大量的註釋來說明解釋,那麼工程師應該思考是否可以優化程式碼表現力。

    ​   其次,氾濫的註釋不但不能幫助工程師理解程式碼,而且會影響程式碼的可讀性,甚至會增加程式的維護成本。如下示例程式碼是濫用註釋的樣例,方法名 put,加上兩個有意義的變數名elephant和fridge,已經明確表達了程式碼功能,完全不需要額外的註釋。在遇到修改程式碼邏輯時,註釋氾濫會帶來災難性的負擔。

    // put elephant into fxidge 
    put (elephant, fridge);
    
  3. Advance with the times

    ​   與時俱進的重要性對於開發工程師來說是不言而喻的。就像道路狀況與導航軟體一樣,如果導航軟體嚴重滯後,就失去了導航的意義。同樣,針對一段有註釋的程式碼,如果程式設計師修改了程式碼邏輯,但是沒有修改註釋,就會導致註釋無法跟隨程式碼前進的腳步,誤導後續開發者。因此,任何對程式碼的修改,都應該同時修改註釋。

註釋格式

註釋格式主要分為兩種∶ 一種是 Javadoc 規範,另一種是簡單註釋。

  1. Javadoc規範

​   類、類屬性和類方法的註釋必須遵循Javadoc規範,使用文件註釋(/***/)的格式。按 Javadoc 規範編寫的註釋,可以生成規範的 JavaAPI 文件,為外部使用者提供非常有效的文件支援。而且在使用IDE 工具編碼時,IDE 會自動提示所用到的類、方法等註釋,提高了編碼的效率。

​   這裡要特別強調對列舉的註釋是必需的。有人覺得列舉通常帶了String name 屬性,已經簡要地說明了這個列舉屬性值的意思,此時註釋是多餘的。其實不然,因為∶

​ (1)列舉實在太特殊了。它的程式碼極為穩定。如果它的定義和使用出現錯誤,通常影響較大。

​ (2)註釋的內容不僅限於解釋屬性值的含義,還可以包括注意事項、業務邏輯。如果在原有列舉類上新增或修改一個屬性值,還需要加上建立和修改時間,讓使用者零成本地知道這個列舉類的所有意圖。

​ (3)列舉類的刪除或者修改都存在很大的風險。不可直接刪除過時屬性,需要標註為過時,同時註釋說明過時的邏輯考慮和業務背景。

  1. 簡單註釋

​   包括單行註釋和多行註釋。特別強調此類註釋不允許寫在程式碼後方,必須寫在程式碼上方,這是為了避免註釋的參差不齊,導致程式碼版式混亂。雙畫線註釋往往使用在方法內部,此時的註釋是提供給程式開發者、維護者和關注方法細節的呼叫者檢視的。因此,註釋的作用更應該是畫龍點睛的,通常新增在非常必要的地方,例如複雜演算法或需要警示的特殊業務場景等。

說明:本文內容參考《碼出高效:Java開發手冊》第三章 程式碼風格,有興趣的讀者可以看書的原文。

相關文章