【如何寫乾淨的程式碼 ? 】程式設計 代碼風格 指南 | 基礎 + 9 個進階概念


你寫過爛程式嗎 ? 你寫過好程式嗎 ? 在我的工作職涯中,我是如何發現應該要寫得整潔這件事? 關於寫程式,有一點要知道的是, 軟體之所以是軟體,是因為除了讓電腦的行為符合的需求外, 客戶永遠會想要增加新的功能,而且不想要花太大的代價。 如果你是個剛入行的工程師,那麼讓程式能動的確是你目前最重要的事情, 但如果你的目標是成長為資深的工程師,成為人們口中的「專業人士」的話, 程式代碼寫得清楚明白,會是你邁向這目標的重要哩程碑。 拜讀完無瑕的程式碼一書後,我整理了一份講義教程,分享給大家 裡面會先說明為什麼要有編程風格? 再來會告訴你如何使程式碼整潔? 以及 Java 編程風格中通用的慣例與細節 ! 最後則會告訴你如何開始做這件事情 !!!


文件 

目錄

  • Java 類別結構 (class structure)
  • 基礎 (basic)
    • 套件 (package)
    • 導入 (import)
    • 類別 (class) / 介面 (interface) / 實作 (implements)
    • 常態 (CONTSTANT) / 變數 (variable) / 列舉 (enum)
    • 方法 (methods)
      • if...else
      • switch
      • for/while
      • "".equals
      • String / StringBuilder / StringBuffer
      • StringBuilder / StringBuffer
      • try...catch
      • annotation
  • 進階 (Advanced)
    • 混合程式語言
    • if 條件式,正向表述
    • 物件的輸入與輸出
    • 函式參量數量建議
    • 函式不要回傳與傳遞 null 
    • 不要連續呼叫函式
    • 函式僅有一個輸入與輸出
    • 何時使用註解
    • 測試代碼

Java 類別結構 (Class Structure)

Java 類別結構

標準Java 的慣例

公用靜態常數-> 私有靜態常數-> 私有變數-> 公用函式-> 私有工具函式

  • 不使用公用變數
  • 函式緊接在變數宣告後的後方
  • 函式遵循降層法則
    • 私有的工具函式緊接在呼叫它的公用函式後方
    • 私有的工具函式依照呼叫順序至上而下排列

Example

public class AppInfoService {
    public final Integer MAX_NUMBER = 1;

    private final Integer MAX_COUNT = 2;

    private String msg = "";

    public void func01(){
        //...
        subFunc01();
        //... 
    }  

    private void subFunc01()
    {
        //...
    }

    public void func02(){
        //...
        subFunc02();
        //... 
    }  

    private void subFunc02()
    {
        doSomething01();
        doSomething02();
    }

    private void doSomething01(){
        //...
    }

    private void doSomething02(){
        //...
    }

}

基礎(Basic)

套件(package)

套件的命名全小寫,以組織網域(domain name) 相反順序命名,接續模組層級名稱。

Example

Website : 

    http://www.enoxs.com

Package : 

    com.enoxs
    com.enoxs.domain
    com.enoxs.domain.service 

導入(import)

未使用的import 套件移除,避免資訊幹擾

小知識: import package.*

Java 的 import 與 C / C++ 的 #include 不一樣,
無論用單個類別名稱或使用 * 來參照,編譯過的 .class 都是一樣的,
不會影響到執行效能,至多編譯時的時間拉長。

常用做法是使用 IDE 快捷鍵 「代碼補全」與 「清除無效 import」

類別 (class) / 介面 (interface) / 實作 (implements)

類別與介面以大駝峰式命名法(Upper Camel Case)命名,首字母大寫。

實作類別以實作之介面名稱後方接續Impl,表達實作與介面之間的關聯性。

public class AppInfo{
    //...
}

public interface AppInfoService {
    // ...
}

public class AppInfoServiceImpl implements AppInfoService {
    // ...
}

類別(Class)是程式語言中的名詞

命名應該具體,不可以模稜兩可,應避免 Custom / Common / Super 等含糊字眼作為前綴詞,
會導致過多的不適當的任務被分配到此類別中。

同樣適用「簡短」與「小」原則,名稱越短越好,類別越小越好,他應該如同貼著標籤的小抽屜詳細分類,而不是一個大箱子裝著眾多雜物。

常數(CONTSTANT) / 變數(variable) / 列舉(enum)

常數

常數命名全大寫,名詞以底線區隔,前方加上final修飾詞

final int MAX_CONNECT_LIMIT = 100;

變數

變數以小駝峰式命名法(Lower Camel Case)命名,首字母小寫

  • 命名應該精確,表達含義,且越簡短越好
  • 單字符變數,如: ijk,僅迴圈唯一使用
  • 不使用匈牙利命名法,如: sMsgmContext,它的誕生有其年代背景,現在已不在適用。
  • POJO 類別,使用Object 型態,不使用primitive 的type,序列化轉換時int 會預設帶值。
  • 後綴詞可使用特定大寫縮寫,如: VOPODTO/DAO等代表特定用途的Bean物件。
  • 布林值變數多以ishas/can為前綴
isConnected = true;

String welcomeMsg = "Hello";

public class AppInfo {
    private Integer Id;
    private String name;

    public void setId(Integer id){
        this.id = id;
    }

    public Integer getId(){
        return this.id;
    }

    public void setName(String name){
        this.name = name;
    }

    public String getName(){
        return this.name;
    }
}

列舉

若有多個公用靜態常數且為相同性質,使用列舉實作狀態常量

public enum PlayAction {
    START(1) ,
    STOP(2) ,
    PAUSE(3) ,
    NEXT(4) ,
    PREV(5) ;

    private int value;

    private PlayAction(int value){
        this.value = value;
    }

    public int value(){
        return value;
    }
}

方法(methods)

方法以小駝峰式命名法(Lower Camel Case)命名,首字母小寫

public String parseData(){
    // ...
}

函式(function)是程式語言中的動詞

「只做一件事,概念上的一件事。」

公用函式就是暴露在外的最大概念,私有的工具函式就是大概念中的小概念。
大概念包含小概念,同樣適用「簡短」與「小」原則,名稱越短越好,函式越小越好。

並且依照降層法則至上而下排列,明確表達概念的邏輯性。

if...else

單行同樣使用大括號,增加可讀性。

switch

每個語句都包含default 語句,即使他什麼代碼也不包含

for/while

避免迴圈內創建新的物件,節省記憶體創建與垃圾回收處理時間

"".equals()

檢查空字串時,""空字串放在前面,變數放在後面,避免物件為空時觸發NullPointerException

String / StringBuilder / StringBuffer

字串串接時使用StringBuilder 或StringBuffer

  • 速度考量: StringBuilder
  • 多執行緒安全考量: StringBuffer

StringBuilder / StringBuffer

宣告時盡量確定Str​​ingBuffer 的容量,不宣告的話,預設大小16 字元陣列,動態擴增時會佔用效能

try...catch

放置在最外層,不要再迴圈內使用,也避免當成當成if...else 的條件式使用

Annotation

區塊註解

通常寫在函式的上方說明函式的功能,IDE 快捷F1 可以幫助查看說明

/**
* Describe this function
**/

public String parseData(String msg){
    // doSomething
}

單行註解

通常寫在區塊內用來說明流程狀態

// Step01 - convert to transfer data 
String msg = parseData(text);
// Step02 - add header and footer
context.append(header);
context.append(msg);
context.append(footer);

尾部註解

通常說明該行變數或方法狀態

if("".equals(state)){ // api state empty means normal
    // doSomething
}

進階 (Advanced)

混合程式語言

如果情況許可,盡量不要混用程式語言,如: JSP & Java / Html & JavaScript / Java & SQL

能夠獨立的都盡量獨立在各自檔案中,若真無法避免,也建議統一在相近的區塊中,不要像麵條一樣糾纏。

Example

Servlet / JSP 網頁,不使用JSP 語法與調用Java 類別,都盡量使用Ajax 方式異步訪問,Html 與JavaScript 寫在不同檔案中。

if 條件式,正向表述

if 小括號條件式,正向表述

否定式比起肯定式的條件判斷需要在腦袋中多轉換一次,比較不直觀。

Not easy to understand

if(!isLock){ // 功能沒鎖住,代表有權限
    view.show();
    // 執行有權限才能做的任務..
}else{
    view.hide();
}

Suggestion

if(isOpen){
    view.show();
}else{
    view.hide();
}

if 小括號條件式,概念打包

條件包含了不太直觀或太多判斷意圖,應該要封裝這個條件判斷成私有工具函式,並且為這個函式取一個最適當的名稱。

Not easy to understand

public void process(List<AppInfo> lstAppInfo){
    if(lstAppInfo.size() > 5){
        doSomething();
    }
}

Suggestion

public void process(List<AppInfo> lstAppInfo){
    if(isReady(lstAppInfo)){
        doSomething();
    }
}

private boolean isReady(List lstAppInfo){
    return lstAppInfo.size() > 5 ? true : false;
}

物件的輸入與輸出

進入到Java 程式的控制範圍內,都盡量以物件的方式輸入與輸出,面向物件的開發。

  • 網頁訪問的資料,可使用Spring 框架,將xml 或json 序列化為具體物件。
  • 資料庫的持久化,可使用ORM 框架,資料表內容封裝成PO(Persistent Object)物件,操作動作封裝成DAO (Data access object) 物件
  • 若是在沒有使用框架的情況下,則自行實作序列化工具,可使用反射機制實作,有需要進行弱點掃描的情況下,使用BeanInfo物件序列化。

函式參量數量建議

函式的參數數量建議 0 > 1 > 2 >> 3

0 parameter

零個參數是最理想的情況

1 parameter

其次,一個參數,也最常見

2 parameter

兩個參數才能代表的東西,例如:速度-距離與時間、點-X軸與Y軸,但也稍微影響可讀性。

3 parameter

盡量避免,參數的順序性、看到時的停頓,嚴重影響可讀性

>3 parameter

超過三個以上的參數都使用POJO物件來進行封裝,大概念包含小概念

map parameter

盡量不要使用Map 物件傳遞,看不出意圖,異常時還不知道是哪個元素導致

函式不要回傳與傳遞 null

函式不要回傳null ,呼叫函式也不要傳遞null

不要回傳null

給呼叫者增加新的工作量,必須添加null 的檢查,導致程式碼混亂

如果是巢狀if 的話,更可能導致錯誤深埋,除錯困難。

不要傳遞null

你無法保證對方的api 一定有做空值得檢查機制,問題同上。

解決方案

  • 使用JDK8 Optional 類別
  • try...catch 捕捉異常
  • 回傳特殊情況物件

不要連續呼叫函式

不要在同一行中連續呼叫物件函式,其中一個出錯,就會整串出錯,也不利於進行異常問題的追蹤。

String msg = appService.getAppGroup().getAppInfo().getName();

正確的做法

程式碼拆分

AppGroup appGroup = appService.getAppGroup();
AppInfo appInfo = appGroup.getAppInfo();
String msg = appInfo.getName();

函式僅有一個輸入與輸出

函式中都應該只有一個輸入與一個輸出

return

函式中只能有一個return

Not easy to understand

public boolean register(AppInfo appInfo) {
  if(appInfo.getName().length() > 3){
     return false;
  }
  if(appInfo.getName().length() < 15){
     return false;
  }
  return true;
}

Suggestion


public boolean register(AppInfo appInfo) {
   boolean isVaild = false;
   boolean isResigterSucc = false;
   if(appInfo.getName().length() > 3){
      isVaild = true;

   }

   if(isVaild && appInfo.getName().length() < 15){
      isVaild = true;

   }
   if(isVaild){
      isResigterSucc = true;
   }
   return isResigterSucc;
}

call by reference

不要使用傳址呼叫的方式修改資料,接收物件並修改完之後,同樣回傳,

Not easy to understand

public String  process(Map<String, String> map) {
   parserName(map);
   parserMsg(map);

   return map.get("state");
}
private void parserName(Map<String, String> map){
   String name = "header" + map.get("name") + "footer";
   map.put("name",name);
   map.put("state" , "1");
}
private void parserMsg(Map<String, String> map){
   String msg = "Hello";
   map.put("msg",msg);
   map.put("state" , "2");
}

Suggestion

public String  process(Map<String, String> map) {
   Map<String,String> map01 = parserName(map);
   Map<String,String> map02 = parserMsg(map01);
   return map02.get("state");
}
private Map<String,String> parserName(Map<String, String> map){
   String name = "header" + map.get("name") + "footer";
   map.put("name",name);
   map.put("state" , "1");
   return map;
}
private Map<String,String> parserMsg(Map<String, String> map){
   String msg = "Hello";
   map.put("msg",msg);
   map.put("state" , "2");
   return map;
}

何時使用註解

表達當前情緒、紀錄工作日誌、廢除不使用的代碼等跟程式無直接相關的註解都不應該出現。

除非萬不得已,不使用註解。註解的使用時機是在原本的程式碼,無法表達他正在做的事情時,使用註解補充說明。

註解合理使用

  • 法規與法條
  • 領域知識
  • 解釋意圖
  • 標示重要性
  • 待辦清單(TODO)。

測試代碼

重要性等同於程式,或者重要於程式

參考資料

書籍:無瑕的程式碼(Clean Code)

Google Java Style Guide - 

https://google.github.io/styleguide/javaguide.html

留言

熱門文章

Markdown 語法大全,範例模板

【 git 基礎教程 #1】什麼是 git ? | Sourcetree 介紹 與 入門基礎操作教學

【什麼是 git flow ?】 5 項分支全詳解 | Sourcetree 實戰演練