歡迎光臨
每天分享高質量文章

CodeReview 常見程式碼問題( 下 )

(點選上方公眾號,可快速關註)


來源:琴水玉 ,

www.cnblogs.com/lovesqcc/p/9271781.html

可維護性問題

可維護性問題是“在當前業務變更的範圍內通常不會導致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註入。

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

https://book.douban.com/subject/3012601/

安全問題

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

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

設計問題

設計問題通常體現在: a. 是否有潛在的效能問題; b. 是否有安全問題; c. 業務變化時是否容易擴充套件; d. 是否有遺漏的點。

較輕微的問題

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

較輕微問題一般不會造成負面影響的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

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 迴圈的準確性。

系列

【關於投稿】


如果大家有原創好文投稿,請直接給公號傳送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章連結

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂