本文首發於個人微信公衆號《andyqian》, 關注即可獲得一線互聯網內推!
前言
最近一段時間進行了部分代碼的code review。其中有 review 的,也有被review的。在這過程中發現了許多問題,而其中就包含許多本不該發生的。同樣的,這些不該發生的問題如果攜帶上線,隨之而來的則是一個個的生產事故。對於金融系統來說,會直接造成資產損失,而對於醫療軟件而言,甚至涉及生命安全。經過分析分類後,以下幾點是最爲常見的問題:
代碼順序倒置
代碼順序,這一點太容易忽略了,以至於有時讓我們覺得這根本不是問題。話不多說,我們先來看例子:
反例:
public void createUser(String telephone, String name, Integer age){
//1. 校驗手機號
if (StringUitls.isBlank(telephone)) {
...
}
//2. 查詢用戶
User uesr = queryUser(telephone);
//3. 校驗name ...
if (StringUitls.isBlank(name)) {
...
}
//4. 校驗年齡
if (null == age) {
...
}
//5. 保存用戶
saveUser(telephone, name, age);
}
上述代碼中存在若干問題,有以下幾點:
- 代碼順序混亂,其中 查詢用戶 不應該在第二步,爲什麼呢?因爲如果 name 爲空 或 age 爲空時,則會白白浪費一次數據庫查詢,而這次查詢,原本是不該發生的。
- 校驗邏輯和核心邏輯混在一起,造成紅花與綠葉不分。
正例:
public void createUser(String telephone, String name, Integer age) {
//檢查參數
checkCreateUserParams(telephone, name, age)
// 保存用戶
saveUser(telephone, name, age);
}
//校驗參數
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
//保存用戶
private void saveUser(String telephone, String name, Integer age) {
User uesr = queryUser(telephone);
if (user == null) {
saveUser(telephone, name, age);
}
}
建議:實際上,工作中編寫的邏輯會比上述複雜很多,但道理是相通的。代碼合理有效順序,也會帶來諸多好處,例如:減少IO操作(調用DAO方法),較少數據傳輸(調用RPC方法)。而合適的順序應該遵循:參數校驗 優先於 核心流程處理。
無意的 NullPointerException
我們都知道 NPE (NullPointerException) 的判斷是調用者的職責。而在方法實現中,有很多同學稍微不注意,就會導致一些低級的 NPE (NullPointerException) 產生。例如:
反例:
public void processUserInfo(String telephone) {
User uesr = queryUser(telephone);
if (user != null) {
...
}
Long uerId = user.getOid;
}
問題:
此時當 telephone 查詢的用戶不存在時,則會產生 NullPointerException,你還別笑,這類問題發生的頻率還不低,非常值得我們注意。
正例:
public void processUserInfo(String telephone) {
User uesr = queryUser(telephone);
if (user == null) {
return;
}
...
}
職責模糊
在學習設計模式時,第一條設計原則就是:單一職責。這給我們的啓示是:在定義方法時,應該儘量原子,讓其承擔單一職責。但在實踐過程中,則出現許多比較怪的實現,以下面代碼爲例:
反例:
//1. 調用創建用戶
public void invokerCreateUser(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
createUser()
}
//2. 創建用戶原子方法
public void createUser(String telephone, String name, Integer age) {
User uesr = queryUser(telephone);
if (user == null) {
saveUser(telephone, name, age);
}
}
- invokerCreateUser 爲創建用戶調用方。
- createUser 爲創建用戶提供方。
問題:
- invokerCreateUser 作爲 調用方承擔了 本該 createUser 提供方承擔的職責。
建議:
無論在什麼場景下,方法的提供方都需要負責進行參數的校驗,而不是將這些職責委託到調用方。否則諸多調用方都需要進行參數驗證,代碼壞味道隨即而來!同樣的道理,思維發散開來。我們在定義方法時:屬於該方法的職責,我們一個也不能少的承擔,不屬於該方法的職責,一個也不需要承擔。
方法過長
抽取方法這點非常重要,好處包括但不限於以下幾點:
- 方法越小,職責越單一,其複用性越高,從而減少重複代碼的產生。
- 代碼邏輯清晰,可以使主幹代碼更清晰,易於維護。
…
有很多同學,習慣在一個方法中編寫成百上千行的代碼彰顯自己的能力。但事實上這樣的代碼及其不易維護,且閱讀都非常吃勁。那多少行纔是最合適的呢?這一點,在阿里巴巴的《Java手冊》中提到:
推薦:單個方法的總行數不超過 80 行。(除註釋之外的方法簽名、 左右大括號、方法內代碼、空行、回車及任何不可見字符的總行數不超過 80 行。)
同樣也給出了這樣做的原因:代碼邏輯分清紅花和綠葉,個性和共性,綠葉邏輯單獨出來成爲額外方法,使主幹代碼更加清晰;共性邏輯抽取成爲共性方法,便於複用和維護。
以下述方法爲例:
反例:
public void createUser(String telephone, String name, Integer age){
//
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
//用戶不存在則保存
User uesr = queryUser(telephone);
if (user == null) {
saveUser(telephone, name, age);
}
}
正例:
public void createUser(String telephone, String name, Integer age) {
//檢查參數
checkCreateUserParams(telephone, name, age)
// 保存用戶
saveUser(telephone, name, age);
}
//校驗參數
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
//保存用戶
private void saveUser(String telephone, String name, Integer age) {
User uesr = queryUser(telephone);
if (user == null) {
saveUser(telephone, name, age);
}
}
無用代碼引用
去除無用代碼,其實有很多方面都是需要注意的,其中包括但不限於以下幾點:
- 去除未使用的 import。(window 可用快捷鍵:Ctrl+Alt + O 進行格式化)。
- 去除未使用的局部變量。
反例:
private void checkCreateUserParams(String telephone, String name, Integer age) {
//1. 未使用的局部變量 sex
Integer sex = "";
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
正例:
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
3. 移除未使用的方法
反例:
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
// 未使用的私有方法
private void check(){
return ;
}
正例:
private void checkCreateUserParams(String telephone, String name, Integer age) {
if (StringUitls.isBlank(telephone)) {
...
}
if (StringUitls.isBlank(name)) {
...
}
if (null == age) {
...
}
}
建議:
- 代碼中未使用到的代碼,及時刪除,特別是私有方法。已經上線的RPC方法,則採取版本遞進的方法進行移除。未上線且未使用的RPC方法及時刪除,否則上線完後刪除的風險就會非常大。
掛羊頭賣狗肉
這一類問題,其實在接口定義上出現的比較多。其簡單來說:就是表裏不一,方法中返回對象,方法名,方法請求對象三者不統一。這樣的接口提供給調用者,調用者會非常疑惑,甚至想過來懟你兩句!以下述代碼爲例:
反例:
private User saveUserInfo(String telephone) {
return queryUser(telephone);
}
問題: 方法名爲 saveUser,其字面意思是:保存用戶信息,但其內部實現則爲查詢用戶信息,明顯的掛羊頭賣狗肉。
正例:
private User getUser(String telephone) {
return queryUser(telephone);
}
建議:
- 定義接口時,接口返回對象,方法名,請求參數 三者語義要統一,避免上述問題產生。
- 方法名儘量能夠表達一個行爲,例如:get / remove / put 等,而不是用名詞來代替。
小結
上述幾種問題是最近 code review 過程中出現最多的幾類問題以及解決辦法。還有一些諸如 code style 類的問題,沒有在本文中描述,不過還是建議大家一定要抽時間看看《Java手冊》,每次看都有新的收穫!還有就是一些提升代碼質量的途徑,諸如:
- 回看一週前,一月前自己寫的代碼,對不滿意的代碼進行重構。
- 參加開源項目,給開源項目提交 Pull Request,提交的代碼會有多人進行code review,直至滿足規範後,纔會進行 merge。
相關閱讀: