命名篇
1、有意思的命名
1.1、變量名
變量名應該是名詞,能夠正確地描述業務,有表達力。如果一個變量名需要註釋來補充說明,那麼很可能說明命名就有問題。
int d ; //標識過去的天數
int elapsedTimeInDays;
# 魔術數--見名知意
SECONDS_PER_DAY -> 86400
PAGE_SIZE -> 10
1.2、函數名
函數命名要具體,空泛的命名沒有意義。
processData() 差於 validateUserCredentials() 或 eliminateDuplicateRequests()
getLatestEmployee() 好於 popRecord()
1.3、類名
類是面向對象中最重要的概念之一,是一組數據和操作的封裝。對於一個應用系統,我們可以將類分爲兩大類:實體類和輔助類。
實體類承載了核心業務數據和核心業務邏輯,其命名要充分體現業務語義,並在團隊內達成共識,如Customer
、Bank
和Employee
等。
輔助類是輔佐實體類一起完成業務邏輯的,其命名要能夠通過後綴來體現功能。例如,用來爲Customer
做控制路由的控制類CustomerController
、提供Customer
服務的服務類CustomerService
、獲取數據存儲的倉儲類CustomerRepository
。
對於輔助類,儘量不要用Helper
、Util
之類的後綴,因爲其含義太過籠統,容易破壞SRP
(單一職責原則)。比如對於處理CSV
,可以這樣寫:
CSVHelper.parse(String)
CSVHelper.create(int[])
但是我更建議將CSVHelper拆開:
CSVParser.parse(String)
CSVBuilder.create(int[])
1.4、包名
包(Package)代表了一組有關係的類的集合,起到分類組合和命名空間的作用。
包名應該能夠反映一組類在更高抽象層次上的聯繫。例如,有一組類Apple
、Pear
、Orange
,我們可以將它們放在一個包中,命名爲fruit
。
包的命名要適中,不能太抽象,也不能太具體。此處以上面提到的水果作爲例子,如果包名過於具體,比如Apple,那麼Pear和Orange放進該包中就不恰當了;如果報名太抽象,稱爲Object,而Object無所不包,這就失去了包用來限定範圍的作用。
1.5、模塊名
相對於包來說,模塊的粒度更大,通常一個模塊中包含了多個包。
名稱要反映模塊在系統中的職責。因此,對任何應該遵循COLA規範的應用都有着xxx-controller
、xxx-app
、xxx-domain
和xxx-Infrastructure
這4個標準模塊。
2、保持一致性
保持命名的一致性,可以提高代碼的可讀性,從而簡化複雜度。因此,我們要小心選擇命名,一旦選中,就要持續遵循,保證名稱始終一致。
2.1、每個概念一個詞
每個概念對應一個詞,並且一以貫之。
例如,fetch、retrieve、get、find和query都可以表示查詢的意思,如果不加約定地給多個類中的同種查詢方法命名,你怎麼記得是哪個類中的哪個方法呢?同樣,在一段代碼中,同時存在manager、controller和handler,會令人感到困惑。
在實際項目中,按照以下約定,保持命名的一致性:
CURD操作 | 方法名約定 |
---|---|
新增 | create |
添加 | add |
刪除 | remove |
修改 | update |
查詢(單個結果) | get |
查詢(多個結果) | list |
分頁查詢 | page |
統計 | count |
2.2 使用對仗詞
遵守對仗詞的命名規則有助於保持一致性,從而提高代碼的可讀性。
下面列出一些常見的對仗詞組:
- add/remove
- increment/decrement
- open/close
- begin/end
- insert/delete
- show/hide
- create/destroy
- lock/unlock
- source/target
- first/last
- min/max
- start/stop
- get/set
- next/previous
- up/down
- old/new
2.3 後置限定詞
如果你要用類似Total
、Sum
、Average
、Max
、Min
這樣的限定詞來修改某個命名,那麼記住把限定詞加到名字的最後,並在項目中貫徹執行,保持命名風格的一致性。
這種方法有很多優點。首先,變量名中最重要的部分,即爲這一變量賦予主要含義的部分應位於最前面,這樣可以突出顯示,並會被首先閱讀到。其次,可以避免同時在程序中使用totalRevenue
和revenueTotal
而產生的歧義。如果貫徹限定詞後置的原則,我們就能收穫一組非常優雅、具有對稱性的變量命名,例如revenueTotal
(總收入)、expenseTotal
(總支出)、revenueAverage
(平均收入)和expenseAverage
(平均支出)。
需要注意的一點是Num這個限定詞,Num放在變量名的結束位置表示一個下標,customerNum表示的是當前客戶的序號。爲了避免Num帶來的麻煩,我建議用Count或者Total來表示總數,用Id表示序號。這樣,customerCount表示客戶的總數,customerId表示客戶的編號。
3、自明的代碼
3.1、中間變量
我們可以通過添加中間變量讓代碼變得更加自明,即將計算過程打散成多個步驟,並用有意義的變量名來命名中間變量,從而把隱藏的計算過程以顯性化的方式表達出來。
例如,我們要通過Regex來獲得字符串中的值,並放到map中。
Marcher matcher = headerPattern.matcher(line);
if(matcher.find()) {
headers.put(matcher.group(1), matcher.group(2));
}
用中間變量,可以寫成如下形式:
Marcher matcher = headerPattern.matcher(line);
if(matcher.find()) {
String key = matcher.group(1);
String value = matcher.group(2);
headers.put(key, value);
}
中間變量的這種簡單用法,顯性地表達了第一個匹配組是key
,第二個匹配組是value
。只要把計算過程打散成一系列良好命名的中間值,不透明的語義自然會變得透明。
3.2、設計模式語言
使用設計模式語言也是代碼自明的重要手段之一,在技術人員之間共享和使用設計模式語言,可以極大地提升溝通的效率。當然,前提是大家都要理解和熟悉這些模式,否則就會變成“雞同鴨講”。因此,我們有必要在命名上就將設計模式顯性化出來,這樣閱讀代碼的人能很快領會到設計者的意圖。
例如,Spring
裏面的ApplicationListener
就充分體現了它的設計和用處。通過這個命名,我們知道它使用了觀察者模式,每一個被註冊的ApplicationListener
在Application
狀態發生變化時,都會接收到一個notify。這樣我們就可以在容器初始化完成之後進行一些業務操作,比如數據加載、初始化緩存等。
3.3、小心註釋
- 不要複述功能
- 要解釋背後意圖
4、規範
4.1、命名規範
在Java中,我們通常使用如下命名約定。
- 類名採用“大駝峯”形式,即首字母大寫的駝峯,例如
Object
、StringBuffer
、FileInputStream
。 - 方法名採用“小駝峯”形式,即首字母小寫的駝峯,方法名一般爲動詞,與參數組成動賓結構,例如
Thread
的sleep(long millis)
、StringBuffer
的append(String str)
。 - 常量命名的字母全部大寫,單詞之間用下劃線連接,例如
TOTAL_COUNT
、PAGE_SIZE
等。 - 枚舉類以
Enum
或Type
結尾,枚舉類成員名稱需要全大寫,單詞間用下劃線連接,例如SexEnum.MALE
、SexEnum.FEMALE
。 - 抽象類名使用
Abstract
開頭;異常類使用Exception
結尾;實現類以Impl
結尾;測試類以它要測試的類名開始,以Test
結尾。
4.2、日誌規範
詳細的日誌輸出級別分爲OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者自定義的級別。我認爲比較有用的4個級別依次是ERROR、WARN、INFO和DEBUG。通常這4個級別就能夠很好地滿足我們的需求了。
- 1.ERROR級別
ERROR表示不能自己恢復的錯誤,需要立即被關注和解決。例如,數據庫操作錯誤、I/O錯誤(網絡調用超時、文件讀取錯誤等)、未知的系統錯誤(NullPointerException、OutOfMemoryError等)。
對於ERROR,我們不僅要打印線程堆棧,最好打印出一定的上下文(鏈路TraceId、用戶Id、訂單Id、外部傳來的關鍵數據),以便於排查問題。
ERROR要接入監控和報警系統。ERROR需要人工介入處理,及時止損,否則會影響系統的可用性。當然也不能濫用ERROR,否則就會出現“狼來了”的情況。我在實際工作中曾碰到過系統每天會發出上千條錯誤報警的情況,導致根本沒有人看報警內容,在真正出現問題時,也沒有人關注,從而引發線上故障。因此,一定要做好ERROR輸出的場景定義和規範,再配合監控治理,雙管齊下,確保線上系統的穩定。 - 2.WARN級別
對於可預知的業務問題,最好不要用ERROR輸出日誌,以免污染報警系統。例如,參數校驗不通過、沒有訪問權限等業務異常,就不應該用ERROR輸出。
需要注意的是,在短時間內產生過多的WARN日誌,也是一種系統不健康的表現。因此,我們有必要爲WARN配置一個適當閾值的報警,比如訪問受限WARN超過100次/分,則發出報警。這樣在WARN日誌過於頻繁時,我們能及時收到系統報警,去跟進用戶問題。例如,如果是產品設計上有缺陷導致用戶頻繁出現操作卡點,可以考慮做一下流程或者產品上的優化。 - 3.INFO級別
INFO用於記錄系統的基本運行過程和運行狀態。
通常來說,優先根據INFO日誌可初步定位,主要包括系統狀態變化日誌、業務流程的核心處理、關鍵動作和業務流程的狀態變化。適當的INFO可以協助我們排查問題,但是切忌把INFO當成DEBUG使用,這樣會導致記錄的數據過多,一方面影響系統性能,日誌文件增長過快,消耗不必要的存儲資源;另一方面也不利於閱讀日誌文件。 - 4.DEBUG級別
DEBUG是輸出調試信息,如request/response的對象內容。在輸出對象內容時,要覆蓋Object的toString方法,否則輸出的是對象的內存地址,就起不到調試的作用了。通常在開發和預發環境下,DEBUG日誌會打開,以方便開發和調試。而在線上環境,DEBUG開關需要關閉,因爲在生產環境下開啓DEBUG會導致日誌量非常大,其損耗是難以接受的。只有當線上出現bug或者棘手的問題時,纔可以動態地開啓DEBUG。爲了防止日誌量過大,我們可以採用分佈式配置工具來實現基於requestId判斷的日誌過濾,從而只打印我們所需請求的DEBUG日誌。
4.3、異常規範
4.3.1、異常處理
建議在業務系統中設定兩個異常,分別是BizException(業務異常)和SysException(系統異常),而且這兩個異常都應該是UncheckedException。
爲什麼不建議用Checked Exception呢?
因爲它破壞了開閉原則。如果你在一個方法中拋出了Checked Exception,而catch語句在3個層級之上,那麼你就要在catch語句和拋出異常處理之間的每個方法簽名中聲明該異常。這意味着在軟件中修改較低層級時,都將波及較高層級,修改好的模塊必須重新構建、發佈,即便它們自身所關注的任何東西都沒有被改動過。這也是C#、Python和Ruby語言都不支持Checked Exception的原因,因爲其依賴成本要高於顯式聲明帶來的收益。
最後,針對業務異常和系統異常要做統一的異常處理,類似於AOP,在應用處理請求的切面上進行異常處理收斂,其處理流程如下:
try {
// 業務處理
Response res = process(request);
} catch(BizException e) {
// 業務異常使用 warn 級別
logger.warn("BizException with error code:{}, error message:{}", e.getErrorCode, e.getErrorMeg());
}catch (SysException ex){
//系統異常使用 error 級別
logger.error("System error" + ex.getMessage(), ex);
}catch (Exception ex) {
//兜底
logger.error("System error" + ex.getMessage(), ex);
}
千萬不要在業務處理內部到處使用try/catch打印錯誤日誌,這樣會使功能代碼和業務代碼纏繞在一起,讓代碼顯得很凌亂,並且影響代碼的可讀性。
4.3.2、錯誤碼
(1)編號錯誤碼
對於平臺、底層系統或軟件產品,可以採用編號式的編碼規範,好處是編碼風格固定,給人一種正式感;缺點是必須要配合文檔才能理解錯誤碼代表的意思。
例如,數據庫軟件Oracle中總共有2000多個異常,其編碼規則是ORA-00001~ORA-02149,每一個錯誤碼都有對應的錯誤解釋。
ORA-00001:違反唯一約束條件。
ORA-00017:請求會話以設置跟蹤事件。
ORA-00018:超出最大會話數。
ORA-00019:超出最大會話許可數。
ORA-00023:會話引用進程私用內存;無法分離會話。
ORA-00024:單一進程模式下不允許從多個進程註冊。
要注意,對不同的錯誤波段,一定要預留足夠的碼號。例如,淘寶開放平臺所用的3位數就顯得有些拘謹,其支撐的錯誤數最多不能超過100,超過100後,爲了向後兼容,只能通過子錯誤碼的方式進行變通處理。
(2)顯性化錯誤碼
顯性化的錯誤碼具有更強的靈活性,適合敏捷開發。例如,我們可以將錯誤碼定義成3個部分:類型+場景+自定義標識。每個部分之間用下劃線連接,內容以大駝峯的方式書寫。這裏可以打破Java的常量命名規範,駝峯方式會更方便閱讀。
對於錯誤類型,我們可以做一個約定:P代表參數異常(ParamException)、B代表業務異常(BizException)、S代表系統異常(SystemException)。一個完整的示例如表2-1所示。
錯誤類型 | 錯誤碼約定 | 舉例 |
---|---|---|
參數異常 | P_XX_XX | P_Customer_NameNull:客戶姓名不能爲空 |
業務異常 | B_XX_XX | B_Customer_NameAlreadyExist:客戶姓名已存在 |
系統異常 | S_XX_XX | S_Unknow_Error:未知系統錯誤 |
如果業務應用的錯誤都用這種約定來描述和表達,那麼只要大家都遵守相同的規範,系統的可維護性和可理解性就會大大提升。