CodeReview常見程式碼問題

AlbenXie發表於2018-09-07

路線圖

CR程式碼問題
 

常見程式碼問題

常見的潛在程式碼問題是當前直接會導致BUG、故障或者產品功能不能正常工作的類別。
 

空值

空值恐怕是最容易出現的地方之一。 常見錯誤有: a. 值為NULL導致空指標異常; b. 引數字串含有前導或字尾空格沒有Trim導致查詢為空。 導致以上結果的原因主要有: 無此記錄、有此記錄但由於SQL訪問異常而沒查到、網路呼叫失敗、記錄中有髒資料、引數沒傳。

原則上,對於任何異常, 希望能夠列印出具體的錯誤資訊,根據錯誤資訊很快明白是什麼原因, 而不是一個 null ,還要在程式碼裡去推敲為什麼為空。這樣我們必須識別出程式中可能的null, 並及時檢測、捕獲和丟擲異常。

對於空值,最好的防護是“防禦式程式設計”。當獲取到物件之後, 使用之前總是判斷是否為空,並適當丟擲異常、打錯誤日誌或做其它處理。 有的人嫌檢測為空的 if 語句充斥在程式碼裡會破壞程式碼的可維護性, 對此我的建議是:

  • 空值檢測一定要有, 有勝於無。
  • 在空值檢測總是存在的前提下, 可以優化空值檢測的方法和存在形式。 比如集中於一個類 NullChecker 中管理,並與系統的整體錯誤處理設計保持一致。集中管理和處理一致性原則可以作為系統設計的一個準則。 這樣主流程中只要增加一行呼叫即可, 既可以天網恢恢疏而不漏地檢測物件為空, 也不會讓程式碼顯得難看。
class NullChecker {
       public static void checkNull(Object obj, Error error) {
               if (obj == null)  { throw new BizException(error); }
       }
}
  • 在引數入口處統一做 trim。 如果在業務邏輯裡做 trim , 就會導致有的業務邏輯做了 trim , 有的沒做, 體現在產品上就會有令使用者困惑的事情發生。 比如搜尋和匯出業務, 搜尋能搜尋出來, 匯出卻沒有。
     

未捕獲潛在的異常

第二個容易出錯的地方是未捕獲潛在的異常。呼叫API介面、庫函式或系統服務等,只顧著享受便利卻不做防護,常導致因為區域性失敗而影響整體的功能。最好的防護依然是“防禦式程式設計”。 要麼在當前方法捕獲異常並返回合適的空值或空物件,要麼拋給高層處理。

切不可默默"吞掉錯誤和異常"。 如果這樣做了, 出問題了等著加班和耗費大量腦細胞吧!
在CodeReview的時候一定要仔細詢問:這裡是否可能會丟擲異常?如果拋異常會怎麼處理?是否會影響整體服務和返回結果?
 

低效能

低效能會導致產品功能不好用、不可用,甚至導致產品失敗。

常見情況有:a. 迴圈地逐個呼叫單個介面獲取資料或訪問資料庫; b. 重複建立幾乎完全相同的(開銷大的)物件;c. 資料庫訪問、網路呼叫等服務未處理超時的情況; d. 多重迴圈對於大資料量處理的演算法效能低;e. 大量字串拼接時使用了String而非StringBuilder.

對於 a,最好提供批量介面或批量併發獲取資料; 對於 b, 將可複用物件抽離出迴圈,一次建立多次使用; 對於 c,設定合理的超時時間並捕獲超時異常處理; 對於 d,使用預排序或預處理, 構造合適的資料結構, 使得演算法平均效能在 O(n) 或 O(nlogn) ; 對於 e, 記住: 少量字串拼接使用String, 大量字串拼接使用 StringBuilder, 通常不會使用到 StringBuffer.
 

影響範圍過大

對多個模組依賴的公共函式的修改,容易造成影響範圍超過當前業務改動,無意識地破壞依賴於該公共函式的其他業務。要特別慎重。可靠的方式是:先檢視該公共函式的呼叫, 如果只有自己的業務用,可適當大膽一些; 如果有多個地方依賴,抽離一個新的函式,抽離原函式裡的可複用部分,然後基於可複用部分構建新的函式。修改原則遵循“開閉”原則,才能儘可能使改動影響降低到最小化。

基類及例項欄位和方法也屬於公共函式的範疇。 儘量不要修改基類的東西。
 

單測問題

單測是保證工程質量的第一道重要防線。單測問題一般包括: a. 單測未全部通過; b. 重要業務邏輯缺乏單測; c. 缺乏異常單測; d. 程式碼變更或BUG修復缺乏單測。

單測全部通過應當是提交程式碼到程式碼庫以及程式碼Review的前提條件。程式碼提交者應當保證單測全部通過。沒有捷徑可走。僅當單測全部通過才提交到程式碼庫, 可以通過工具自動化實現。 對於 maven 管理的工程, 只需一個命令: mvn test && git push origin branch_name 。 單測應當更注重質,而非單純追求覆蓋率。

缺乏單測的重要業務邏輯就像裸露在空氣中的電線一樣,雖然能跑起來,卻是很容易“觸電”的。 方法: 增加覆蓋比較全面的單測。

缺乏異常單測也是程式碼提交者常忽略的問題。 異常也是一種實際的業務場景,反映系統的健壯性和友好性。異常應該有相應的單元測試覆蓋。建立條件使之丟擲異常,並判斷異常是否是指定異常;若沒有丟擲異常或者不是指定異常,則應該 AssertFailed 而不是通過。

對於程式碼變更和BUG修復,如果當時由於時間緊而沒有寫,後續應當補上。對於每個程式碼變更和BUG,都可以抽離出相應的程式碼部分, 並有相應單測覆蓋,並註明原因。
 

與原有業務邏輯不相容

改動針對當前需求是合理的,卻與原有業務邏輯不相容,也是常見的問題。比如增加一個搜尋條件, 卻不能與原有條件聯合查詢。

與原有業務不相容, 一般出現在:

  1. 一對一與一對多的變化。 比如原來的關係是一個訂單對應一個物流資訊, 後來變化為一個訂單可能對應多個物流資訊; 原來的邏輯是一個訂單顯示多個物流資訊可以更改,後來要求一個訂單隻展示最近一次的物流資訊可以修改。
  2. 多個業務組合。 業務 A 與業務 B 原來是分開發展的, 後來開展一種活動,將業務A與業務B進行一種組合營銷。 此時,多半會出現很多 if-else 語句。

業務邏輯的相容問題一般體現在系統的複用性和可擴充套件機制上。良好的系統可複用性和可擴充套件性可以更容易地做到業務邏輯相容。 主要有如下幾種級別:

  1. 自動相容。 增加一種型別, 只是 biz_type 的值多了一種, 系統自動將已有功能適配給新的 biz_type;
  2. 一點改動。增加一個分支語句, 對 biz_type 的某個特性進行擴充套件;
  3. 一些改動。 需要見縫插針地增加一個單獨的分支判斷和邏輯處理模組, 對整體可擴充套件性沒有影響, 但會造成區域性的複雜化;
  4. 一部分功能改動。 只需要對其中一個功能模組做個擴充套件;
  5. 多處改動。 需要對多個功能模組做相應的改造,不過更多是新增而不是修改;
  6. 難以改動。 需要深入到功能模組內部做艱難的修改, 並要保證原有功能不受影響。

如何應對呢?

  1. 針對關聯關係, 在專案之初, 可以詢問清楚: 將來在產品上是否有可擴充套件的變化? 及早預留空間, 或者確定產品上的對策; 在程式碼實現上, 兼顧考慮一對一到一對多,或一對多到一對一的關聯變化。比如使用列表來表達單個資訊, 使用索引從列表中獲取單個資訊。
  2. 針對業務組合, 明確各業務的核心部分, 抽離出業務的可複用的部分,形成 API ; 考慮組合模式和裝飾器模式來進行擴充套件。

核心不變, 外圍定製化。
 

缺乏必要日誌

對於重要而關鍵的例項狀態、程式碼路徑及API呼叫,應當新增適當的INFO日誌;對於異常,應當捕獲並新增Error日誌。缺乏日誌並不會影響業務功能,但出現問題排查時,就會非常不方便,甚至錯失極寶貴的機會(不易重現的情況尤其如此)。此外,缺乏日誌也會導致可控性差,難以做資料統計和分析。
 

錯誤碼不符合規範

錯誤碼本身不算是程式碼問題,不過基於整個組織和工程的可維護性來說,可以將錯誤碼不符合規範作為一種錯誤加以避免。方法: 對錯誤碼進行可控的管理和遵循規範使用。可以使用公共文件維護, 也可以開發錯誤碼管理系統來避免相同的錯誤碼。
 

引數檢測缺乏或不足

引數檢測是對業務處理的第一層重要過濾。如果引數檢測不足夠,就會導致髒資料進入服務處理,輕則導致異常,重則插入髒資料到資料庫,對後續維護都會造成很多維護成本。方法: 採用“契約式程式設計”,規定前置條件,並使用單測進行覆蓋。

對於複雜的業務應用, 優雅的引數檢測處理尤為重要。 根據 “集中管理和處理一致性原則”, 可以建立一個 paramchecker 包, 設計一個可複用的微框架來對應用中所有的引數進行統一集中化檢測。引數檢測主要包括: (1) 引數的值型別, 可以根據不同值型別做基礎的檢測; (2) 引數的業務型別, 有基礎非業務引數, 基礎業務引數和具體業務引數。 不同的引數業務型別有不同的處理。 將引數值型別與引數業務型別結合起來, 結合一致性的異常捕獲處理, 就可以實現一個可複用的引數檢測框架。引數檢測既可以採用普通的分支語句,也可以採用註解方式。採用註解方式更可讀,不過單測編寫更具技巧。
 

引用錯誤

對於動態語言, 由於缺乏強大的靜態程式碼檢測,修改了類引用的地方尤其要注意,很可能導致依賴的其他業務出錯; 尤其是修改重名引用時。有線上故障教訓。PHP工程中含有兩個 Format 類, 一個基礎的一個業務相關的, 被改動的類檔案裡開始沒有指明引用,預設採用了基礎 Format 類的實現, 然後提交者在改動檔案頭增加了對業務 Format 的引用, 導致依賴於基礎Format類的其他業務不能正常工作。避免引用錯誤的方法: 當要在檔案裡增加新的類引用時, 先在檔案裡搜尋是否有重名類的引用。如果有, 就要格外小心了。
 

名字衝突

引用錯誤實際上是名字衝突的一種情形。名字衝突常常出現在自定義函式命名跟庫函式名字一樣的情況下。此時,自定義函式的定義會覆蓋庫函式,導致在某一處正常,而其他地方出問題。因此,在命名時要足夠有意識,避免和庫函式命名衝突。
 

細節錯誤

比如邏輯運算子誤寫、優先順序錯誤、長整型截斷、溢位、陣列越界、JSON解析出錯、函式引數傳遞出錯、API 版本不對、使用網上拷貝的未經測試的程式碼、不成熟的演算法、傳值與傳引用、相等性比較等。

對於陣列越界錯誤, 通常要對空陣列、針對陣列大小的邊界值+1和-1寫單測來避免; 使用網上拷貝的程式碼,誠然可節省時間,也一定要加工一下並用單測覆蓋; 傳值和傳引用可通過單測來避免錯誤; 物件的相等性比較切忌使用等號=。
 

多重條件

類似 if ((!A || !B) && C || (D && E)) 的多重條件要仔細推敲。方法: 最好拆分成多個有含義變數。 isNotDelay = !A || !B ; isNormal = C ; isAllow = D && E ; cond = isNotDelay && isNormal || isAllow 。
 

文不符實

文不符實是一種可能導致線上故障的錯誤。比如一個 getXXX 的函式,結果裡面還做了 add, update 的操作。對問題排查、產品運維等都有非常大的殺傷力。因此命名一定要用實質內容相符,除非是故意搞破壞。
 

跨語言或跨系統互動

稍具規模的網際網路創業公司通常會採用多語言開發,比如PHP作為前端,Java作為後臺服務。當動態型別語言與靜態型別語言互動時,會有一些問題產生。比如PHP的物件通常是一個Map, 如果是空物件就會寫成 [], 然而 [] 會被 Java 解析成列表。這樣, 如果資料庫的值是通過 PHP 寫入,那麼這個值既有可能是JSON物件字串,也可能是空陣列字串, Java 來解析就有點尷尬了。 同樣,當 Java 呼叫 PHP 介面時, 不規範的PHP介面既可能返回列表,也可能返回 true or false , Java 解析返回結果也會比較尷尬。 因此, 在跨語言互動的邊界處,要特別注意這些型別轉換的差異。

跨系統互動則主要是介面設計與約定的問題。同一個專案裡不同業務團隊之間的業務介面設計與約定, 不同企業裡開放介面的設計與約定, 要在最初深思熟慮,一旦開放,在後期很少有介面設計改動的空間。開放介面設計要符合小而美、正交的特性, 命名要貼切一致, 引數取值要指明約束,列舉引數要給出列表, 結果返回要規範一致,可以採用通用的 {"code":200, "msg": "success", "data": xxx} 。跨系統互動也要統一對術語和介面的理解的一致。
 

可維護性問題

可維護性問題是“在當前業務變更的範圍內通常不會導致BUG、故障,卻會在日後埋下地雷,引發BUG、故障、維護成本大幅增加”的類別。
 

硬編碼

硬編碼主要有三種情況: a. “魔數”; b. 寫死的配置; c. 臨時加的邏輯和文案。

“魔數”與重複程式碼類似,當前或許不會引發問題,時間一長,為了弄清楚其代表的含義,增加很多溝通維護成本,且分散在各處很容易導致修改的時候遺漏不一致。務必清清除。方法也比較簡單:定義含義明顯的列舉或常量,代表這個魔數在程式碼中發言。

“寫死的配置”不會影響業務功能, 不過在環境變更或系統調優的時候,就顯得很不方便了。 方法: 儘量將配置抽離出來做成配置項放到配置檔案裡。

“臨時加的邏輯和文案”也是一種破壞系統可維護性的做法。方法: 抽離出來放在單獨的函式或方法裡,並特別加以註釋。
 

重複程式碼

重複程式碼在當前可能不會造成 BUG,但上線後,需要維護多處的事實一致性;時間一長,後續修改的時候就特別容易遺漏或處理不一致導致 BUG;重複程式碼是公認的“程式碼壞味”,必當盡力清除。方法: 抽離通用的部分,定製差異。重複程式碼還有一種情況出現,即創造新函式時,先看看是否既有方法已經實現過。
 

通用邏輯與定製業務邏輯耦合

這大概是每個媛猿們在開發生涯中遇到的最噁心的事情之一了。通用邏輯與具體的各種業務邏輯混雜交錯,想插根針都難。遇到這種情況,只能先祈福,然後抽離一個新的函式,嚴格判斷相應條件滿足後去呼叫它。

如果是新建立邏輯,可以使用函數語言程式設計或基於介面的程式設計,將通用處理流程抽離出來,而將具體業務邏輯以回撥函式的形式傳入處理。

不要讓不同的業務共用相同的函式,然後在函式裡一堆 if-else plus switch , 而是每個業務都有各自的函式, 並可複用相同的通用邏輯和流程處理; 或者各個業務可以覆寫同樣命名的函式。

複用,而非混雜。
 

直接在原方法里加邏輯

有業務改動時,猿媛們圖方便傾向於直接在原方法里加判斷和邏輯。這樣做是很不好的習慣。一方面,增加了原方法的長度,破壞了其可維護性;另一方面,有可能對原方法的既有邏輯造成破壞。 可靠的方式是: 新增一個函式,然後在原方法中呼叫並說明原因。
 

多業務耦合

在業務邊界未仔細劃分清晰的情況下出現,一個業務過多深入和摻雜另一個非相關業務的實現細節。在專案和系統設計之初,特別要注意先劃分業務邊界,定義好介面設計和服務依賴關係,再著手開發;否則,延遲到後期做這些工作,很可能會導致重複的工作量,含糊複雜的互動、增加後期系統維護和問題排查的許多成本。磨刀不誤砍柴工。劃分清晰的業務、服務、介面邊界就屬於磨刀的功夫。
 

程式碼層次不合理

程式碼改動邏輯是正確的,然而程式碼的放置位置不符合當前架構設計約定,導致後續維護成本增加。

程式碼層次不合理可能導致重複程式碼。比如獲取操作人和操作記錄,如果寫在類 XController 裡, 那麼類 YController 就面臨尷尬局面: 如果寫在 YController , 就會導致重複程式碼; 如果跨層去呼叫 XController 方法,又是非常不推薦的做法。因此, 獲取操作人和操作記錄,最好寫在 Service 層, Controller 層只負責引數傳入、檢測和結果轉譯、返回。
 

不用多餘的程式碼

工程中常常會有一些不用的程式碼。或者是一些暫時未用到的Util工具或庫函式,或者是由於業務變更導致已經廢棄不用的程式碼,或者是由於一時寫出後來又重寫的程式碼。儘量清除掉不用多餘的程式碼,對系統可維護性是一種很好的改善,同時也有利於CodeReview。
 

使用全域性變數

使用全域性變數並沒有“錯”,錯的是,一旦出現問題,排查和除錯問題起來,真的會讓人“一夜之間白了頭”,耗費數個小時是輕微懲罰。此外,全域性變數還能“順手牽羊”地破壞函式的通用性,導致可維護性變差。務必消除全域性變數的使用。當然,全域性常量是可以的。
 

缺乏必要的註釋

對重要和關鍵點的程式碼缺乏必要的註釋,使用到的重要演算法缺乏必要的引用出處,對特別的處理缺乏必要的說明。

原則上, 每個方法至少要用一個簡短的單行註釋, 適宜地描述了方法的用途、業務邏輯、作者及日期。對於特殊甚至奇葩的需求的特別實現,要加一些註釋。 這樣後續維護時有個基礎。
 

更難發現的錯誤

更難發現的錯誤是指“複雜併發場景下的有一定技術難度的、需要豐富開發與設計經驗才能看出來的錯誤”。
 

併發

併發的問題更難檢測、復現和除錯。常見的問題有:a. 在可能由多執行緒併發訪問的物件中含有共享變數卻沒有同步保護;b. 在程式碼中手動建立缺乏控制的執行緒或執行緒池;c. 併發訪問資料庫時沒有做任何同步措施;d. 多個執行緒對同一物件的互斥操作沒有同步保護。

對於 a, 在大部分Java應用中,通常由Spring框架來控制和建立請求和服務例項,因此,保證“Controller, Service 類中的例項變數只允許 Service, DAO 的單例,不允許業務變數例項”基本確保沒有併發不正確更新的問題;不過,包含快取策略的物件要特別注意多執行緒併發訪問的問題,出於效能考量, 儘量只對共享例項部分加鎖。

對於 b, 禁止在應用中手動建立執行緒或執行緒池,失控的執行緒池很容易導致應用崩潰(有線上應用崩潰的教訓)。

對於 c, 併發訪問資料庫時,要特別注意時序和狀態同步。如果時序控制不對,會導致狀態同步和更新出錯。

對於 d, 對同一物件的互斥操作需要加分散式鎖同步。

使用執行緒池、併發庫、併發類、同步工具而不是執行緒物件、併發原語。在複雜併發場景下,還需注意多個同步物件上的鎖是否按合適的順序獲得和釋放以避免死鎖,相應的錯誤處理程式碼是否合理。
 

資源洩露

  • 開啟檔案卻沒有關閉;
  • 連線池的連線未回收;
  • 重複建立的指令碼引用沒有置空,無法被回收;
  • 已使用完的集合元素始終被引用,無法被回收;

事務

事務方面常出現的問題是:多個緊密關聯的業務操作和 SQL 語句沒有事務保證。 在資金業務操作或資料強一致性要求的業務操作中,要注意使用事務,保證資料更新的一致性和完整性。
 

SQL問題

SQL的正確性通常可以通過 DAO 測試來保證。 SQL問題主要是指潛在的效能問題和安全問題。

要避免SQL效能問題, 在表設計的時候就要做好索引工作。在表資料量非常大的情況下,SQL語句編寫要非常小心。查詢SQL需要新增必要索引,新增合適的查詢條件和查詢順序,加快查詢效率, 避免慢查; 儘量避免使用 Join, 子查詢;避免SQL隱碼攻擊。

尤其避免在 update 語句中使用 where-if ! 很容易導致全表更新和嚴重的資料丟失,造成嚴重的線上故障 !!!

SQL優秀書籍推薦: SQL語言藝術
 

安全問題

安全問題一向是網際網路產品研發中極容易被忽視、而在爆發後又極引發熱議的議題。安全和隱私是使用者的心理紅線之一。應用、資料、資金的安全性應當僅次於產品功能的準確性和使用體驗。

比如:緩衝區溢位; 惡意程式碼注入;許可權賦予不當; 應用目錄洩露等。

安全問題的CodeReview可參見檢查點清單:資訊保安 。主要是如下措施: a. 嚴格檢查和遮蔽非法輸入; b. 對含敏感資訊的請求加密通訊; c. 業務處理後消除任何敏感私密資訊的任何痕跡; d. 結果返回前在反序列化中清除敏感私密資訊; e. 敏感私密資訊在資料儲存裝置中應當加密儲存; f. 應用有嚴格的角色、許可權、操作、資料訪問分級和控制; g. 切忌暴露伺服器的重要的安全性資訊,防止伺服器被攻擊影響正常服務執行。
 

設計問題

設計問題通常體現在: a. 是否有潛在的效能問題; b. 是否有安全問題; c. 業務變化時是否容易擴充套件; d. 是否有遺漏的點; e. 持續高負荷壓力下是否會崩潰。
 

較輕微的問題

較輕微問題是指“沒有技術難度、通過良好習慣即可避免的問題”。

較輕微問題一般不會造成負面影響的BUG或故障,不過建立一些好的習慣,主動使用程式碼檢測工具,消除這些較輕微錯誤,也是一種修行。
 

命名不貼切

命名不貼切不會影響功能實現,卻會誤導理解或增加理解難度。

方法:先查查字典,找個通俗易懂而且比較貼近的名字。可以參考 jdk 的命名、通用詞彙和行業詞彙; 作用域小的採用短命名,作用域大的採用長命名。取名字是一種重要技能,—— 多少父母為此愁灰了頭!
 

宣告時未初始化

宣告時未初始化通常情況下都不會是問題,因為後面會進行賦值。不過,如果賦值的過程中出現異常,那麼可能會返回空值,從而導致空值異常。通常,變數宣告時賦予預設初始值是個好習慣。
 

風格與整體有不一致

工程通常求穩,一致效能更好地維護。在工程專案中,最好能夠遵循工程約定的風格,在個人專案中可以凸顯個性風格。Java程式設計一般要遵循《Java程式設計規範》,有追求的程式猿媛還會追求更高層次的,比如《Google Java 規範》等。
 

型別轉換錯誤

程式語言的型別系統是非常重要的。如何在不同型別之間可靠地互轉,尤其是在父子型別之間相互賦值,也是一個微技能。濫用型別轉換,也會導致BUG 。

Java 中容易出現的錯誤是:a. 字串轉數值,字串含有非數字部分;b. JSON字串轉物件,某個欄位含有不相容的值型別導致解析出錯;c. 子型別轉不相容的父型別,滋生執行時異常 ClassCastException;d. 相同特質的型別不相容。比如 Long 與 Integer 都是數值型,卻不能互轉。

型別轉換中最容易出BUG的地方是非布林型別取反。受C語言的影響,很多高階語言支援各種資料型別轉布林型別,比如 PHP 字串、陣列、數字等都可以轉布林型別,相應的就喜歡寫 if (!notBoolVar) 這種表示式, 容易隱藏看不出的BUG甚至錯誤。
 

否定式風格

變數含義、表示式語句傾向於使用否定式風格,可能不知不覺耗費大量腦細胞,因為每次理解的時候都要繞個彎子。 比如 isNoExpress 是否無需物流, 就有點繞。 為什麼呢? 無需物流是針對快遞發貨的, 如果快遞發貨佔發貨的90%, 無需物流只佔10%,那麼, isNoExpress = false 幾乎總為真。 涉及到判斷的時候,可能不得不寫 if (!isNoExpress) , 雙重否定足夠弄暈你。
 

容器遍歷的結構變更

絕大多數語言都承襲了 C 語言的 for(int i=0;i<N;i++) 迴圈形式。不過,現代程式語言通常都提供了迭代器遍歷、或 foreach 遍歷。 foreach 遍歷通常基於迭代器遍歷實現。 只要對容器結構不做變更,推薦使用 foreach ; 若要遍歷的同時做修改或更新,推薦迭代器模式。 遍歷容器的時候同時做刪除元素操作,要特別留意,很可能導致越界錯誤。更可靠的方式時,直接生成新的容器,如果不涉及空間效率的話。
 

API引數傳遞錯誤

如果API引數有多個,而且相鄰引數的型別相同,那麼要特別留意是否引數順序是正確的,而不會張冠李戴。

當然,在設計API引數的時候,就可以仔細用更精準型別進行區分,並將相同型別的引數錯開。比如 calc(int accountNo, int pay, int timestamp) , 就容易傳錯,比較可靠的是 calc(int accountNo, Currency pay, Timestamp now) ,這樣是不可能將引數傳遞錯誤的。
 

單行呼叫括號過多

為了簡便,常常會寫出 wapper(calc(now, String.format("%s\n", new BufferedFileReader(filename, "UTF-8").readLines() ))) 的語句 , 嗯,你得好好瞧瞧和算算右邊的括號數量是否正確了。更糟糕的時候,結合API引數傳遞錯誤,IDE 可能沒有報錯, 而你很可能沒有意識到自己的引數傳遞錯誤了。 可靠的方式是, 拆出一部分變數,並將呼叫之間的括號用空格隔開,顯示出層次感。

String fileContent = new BufferedFileReader(filename, "UTF-8").readLines();
wapper( calc( now,  String.format("%s\n", fileContent) ) )

 

修改方法簽名

對某個方法有業務改動時,程式猿媛們傾向直接修改原方法的簽名。這時,要特別注意:a. 不要修改原方法的引數順序; b. 在最後面增加可選引數。 從另一個角度來看,複雜的業務方法應當分兩層: 最外層負責排程,方法引數具有包容性,裡面包含的欄位比較多 ; 內層方法負責特定業務邏輯的實現,方法引數少而精。

修改原方法簽名本身就是容易產生問題的習慣, 篡改原方法的引數順序更是大忌。 最好的方法是新建一個方法去複用原方法, 然後呼叫新的方法。程式碼變更始終銘記“開閉”原則。
 

列印日誌太多

列印過多的日誌並不好。一方面遮掩真正需要的資訊,導致排查耗費時間, 另一方面造成伺服器空間浪費、影響效能。生產環境日誌一般只開放 INFO及以上級別的日誌; Debug 日誌只在除錯或排錯的時候使用,生產環境可以禁止debug日誌。
 

多級資料結構

使用多級資料結構時,要確定父級資料一定有值,或者進行檢測。比如 $order['baole']['ump']['money'],必須確保 $order['baole'], $order['baole']['money'] 一定有值或做非空檢測。
 

作用域過大

由於C語言的影響,猿媛們會在開頭就定義好一些變數或要返回的物件,在很靠後的地方才使用到。不必要的過大的作用域對變數和物件的變化產生不可測的影響,並增大理解的成本。可靠的方法是,僅當在使用時才定義,並儘快返回結果。

另一種情況是,暴露的訪問域過大,比如 public 欄位。 儘可能地縮小可訪問的範圍,可以增大變更和重構的空間; 減少可變性,則可以自然地獲得併發安全性,降低CodeReview的理解成本。

比如,不可變的類和欄位定義成 final , 最小化包,類,介面,方法和域的可訪問性,預設為 private , 若需要繼承,可定義為 protected , 僅當需要作為 API 服務暴露出去時,使用 public.
 

分支與迴圈

條件與迴圈偶爾也會導致錯誤, 不過通常錯誤可以在釋出前解決掉。

對於 if-else 巢狀條件, 需要仔細檢查是否符合業務邏輯; 如果巢狀太深,是否可以使用另一種方式“解結” ; 對於 switch 語句, 大多數語言的 case 有 fall through 問題, 要注意加上 break ; 最好加上 default 的處理。

對於 for 迴圈, 編寫合理的結束條件避免死迴圈; 對於迴圈變數的控制, 避免出現 -1或 +1 錯誤, 消除越界錯誤; for 迴圈也要特別注意對空值和空容器的處理,避免丟擲空值異常。可以通過單測來確保 for 迴圈的準確性。
 

殘留的無用程式碼

殘留的無用程式碼,會成為系統的垃圾,增加系統的理解和維護成本。需要及時清理掉。
 

程式碼與文件不一致

文件是理解程式碼的第一扇視窗。優秀的文件,可以極大地降低理解程式碼的成本。但是大多數開發者還並不習慣編寫友好的文件。常常出現無文件、失效文件、誤導性文件等,影響人們的理解和判斷。
 

使用冷僻用法或奇淫巧技

使用冷僻用法或奇淫巧技會增大系統的理解成本,徒然消耗人的腦細胞。思路可借鑑,但不宜用於生產環境中。樸實最宜。

相關文章