【如何寫乾淨的程式碼 ? 】程式設計 代碼風格 指南 | 基礎 + 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)命名,首字母小寫
- 命名應該精確,表達含義,且越簡短越好
- 單字符變數,如:
i
/j
/k
,僅迴圈唯一使用 - 不使用匈牙利命名法,如:
sMsg
/mContext
,它的誕生有其年代背景,現在已不在適用。 - POJO 類別,使用Object 型態,不使用primitive 的type,序列化轉換時int 會預設帶值。
- 後綴詞可使用特定大寫縮寫,如:
VO
/PO
/DTO
/DAO
等代表特定用途的Bean物件。 - 布林值變數多以
is
/has
/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
宣告時盡量確定StringBuffer 的容量,不宣告的話,預設大小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)。
測試代碼
重要性等同於程式,或者重要於程式
留言
張貼留言