相關(guān)閱讀: 互聯(lián)網(wǎng)技術(shù)(java框架、分布式、集群)干貨視頻大全,不看后悔!(免費下載)
可維護性問題 可維護性問題是“在當前業(yè)務(wù)變更的范圍內(nèi)通常不會導致BUG、故障,卻會在日后埋下地雷,引發(fā)BUG、故障、維護成本大幅增加”的類別。 硬編碼 硬編碼主要有三種情況: a. “魔數(shù)”; b. 寫死的配置; c. 臨時加的邏輯和文案。 “魔數(shù)”與重復代碼類似,當前或許不會引發(fā)問題,時間一長,為了弄清楚其代表的含義,增加很多溝通維護成本,且分散在各處很容易導致修改的時候遺漏不一致。務(wù)必清清除。方法也比較簡單:定義含義明顯的枚舉或常量,代表這個魔數(shù)在代碼中發(fā)言。 “寫死的配置”不會影響業(yè)務(wù)功能, 不過在環(huán)境變更或系統(tǒng)調(diào)優(yōu)的時候,就顯得很不方便了。 方法: 盡量將配置抽離出來做成配置項放到配置文件里。 “臨時加的邏輯和文案”也是一種破壞系統(tǒng)可維護性的做法。方法: 抽離出來放在單獨的函數(shù)或方法里,并特別加以注釋。 重復代碼 重復代碼在當前可能不會造成 BUG,但上線后,需要維護多處的事實一致性;時間一長,后續(xù)修改的時候就特別容易遺漏或處理不一致導致 BUG;重復代碼是公認的“代碼壞味”,必當盡力清除。方法: 抽離通用的部分,定制差異。重復代碼還有一種情況出現(xiàn),即創(chuàng)造新函數(shù)時,先看看是否既有方法已經(jīng)實現(xiàn)過。 通用邏輯與定制業(yè)務(wù)邏輯耦合 這大概是每個媛猿們在開發(fā)生涯中遇到的最惡心的事情之一了。通用邏輯與具體的各種業(yè)務(wù)邏輯混雜交錯,想插根針都難。遇到這種情況,只能先祈福,然后抽離一個新的函數(shù),嚴格判斷相應(yīng)條件滿足后去調(diào)用它。 如果是新創(chuàng)建邏輯,可以使用函數(shù)式編程或基于接口的編程,將通用處理流程抽離出來,而將具體業(yè)務(wù)邏輯以回調(diào)函數(shù)的形式傳入處理。 不要讓不同的業(yè)務(wù)共用相同的函數(shù),然后在函數(shù)里一堆 if-else plus switch , 而是每個業(yè)務(wù)都有各自的函數(shù), 并可復用相同的通用邏輯和流程處理; 或者各個業(yè)務(wù)可以覆寫同樣命名的函數(shù)。
直接在原方法里加邏輯 有業(yè)務(wù)改動時,猿媛們圖方便傾向于直接在原方法里加判斷和邏輯。這樣做是很不好的習慣。一方面,增加了原方法的長度,破壞了其可維護性;另一方面,有可能對原方法的既有邏輯造成破壞。 可靠的方式是: 新增一個函數(shù),然后在原方法中調(diào)用并說明原因。 多業(yè)務(wù)耦合 在業(yè)務(wù)邊界未仔細劃分清晰的情況下出現(xiàn),一個業(yè)務(wù)過多深入和摻雜另一個非相關(guān)業(yè)務(wù)的實現(xiàn)細節(jié)。在項目和系統(tǒng)設(shè)計之初,特別要注意先劃分業(yè)務(wù)邊界,定義好接口設(shè)計和服務(wù)依賴關(guān)系,再著手開發(fā);否則,延遲到后期做這些工作,很可能會導致重復的工作量,含糊復雜的交互、增加后期系統(tǒng)維護和問題排查的許多成本。磨刀不誤砍柴工。劃分清晰的業(yè)務(wù)、服務(wù)、接口邊界就屬于磨刀的功夫。 代碼層次不合理 代碼改動邏輯是正確的,然而代碼的放置位置不符合當前架構(gòu)設(shè)計約定,導致后續(xù)維護成本增加。 代碼層次不合理可能導致重復代碼。比如獲取操作人和操作記錄,如果寫在類 XController 里, 那么類 YController 就面臨尷尬局面: 如果寫在 YController , 就會導致重復代碼; 如果跨層去調(diào)用 XController 方法,又是非常不推薦的做法。因此, 獲取操作人和操作記錄,最好寫在 Service 層, Controller 層只負責參數(shù)傳入、檢測和結(jié)果轉(zhuǎn)譯、返回。 不用多余的代碼 工程中常常會有一些不用的代碼?;蛘呤且恍簳r未用到的Util工具或庫函數(shù),或者是由于業(yè)務(wù)變更導致已經(jīng)廢棄不用的代碼,或者是由于一時寫出后來又重寫的代碼。盡量清除掉不用多余的代碼,對系統(tǒng)可維護性是一種很好的改善,同時也有利于CodeReview。 使用全局變量 使用全局變量并沒有“錯”,錯的是,一旦出現(xiàn)問題,排查和調(diào)試問題起來,真的會讓人“一夜之間白了頭”,耗費數(shù)個小時是輕微懲罰。此外,全局變量還能“順手牽羊”地破壞函數(shù)的通用性,導致可維護性變差。務(wù)必消除全局變量的使用。當然,全局常量是可以的。 缺乏必要的注釋 對重要和關(guān)鍵點的代碼缺乏必要的注釋,使用到的重要算法缺乏必要的引用出處,對特別的處理缺乏必要的說明。 原則上, 每個方法至少要用一個簡短的單行注釋, 適宜地描述了方法的用途、業(yè)務(wù)邏輯、作者及日期。對于特殊甚至奇葩的需求的特別實現(xiàn),要加一些注釋。 這樣后續(xù)維護時有個基礎(chǔ)。 更難發(fā)現(xiàn)的錯誤 更難發(fā)現(xiàn)的錯誤是指“復雜并發(fā)場景下的有一定技術(shù)難度的、需要豐富開發(fā)與設(shè)計經(jīng)驗才能看出來的錯誤”。 并發(fā) 并發(fā)的問題更難檢測、復現(xiàn)和調(diào)試。常見的問題有:a. 在可能由多線程并發(fā)訪問的對象中含有共享變量卻沒有同步保護;b. 在代碼中手動創(chuàng)建缺乏控制的線程或線程池;c. 并發(fā)訪問數(shù)據(jù)庫時沒有做任何同步措施;d. 多個線程對同一對象的互斥操作沒有同步保護。 對于 a, 在大部分Java應(yīng)用中,通常由Spring框架來控制和創(chuàng)建請求和服務(wù)實例,因此,保證“Controller, Service 類中的實例變量只允許 Service, DAO 的單例,不允許業(yè)務(wù)變量實例”基本確保沒有并發(fā)不正確更新的問題;不過,包含緩存策略的對象要特別注意多線程并發(fā)訪問的問題,出于性能考量, 盡量只對共享實例部分加鎖。 對于 b, 禁止在應(yīng)用中手動創(chuàng)建線程或線程池,失控的線程池很容易導致應(yīng)用崩潰(有線上應(yīng)用崩潰的教訓)。 對于 c, 并發(fā)訪問數(shù)據(jù)庫時,要特別注意時序和狀態(tài)同步。如果時序控制不對,會導致狀態(tài)同步和更新出錯。 對于 d, 對同一對象的互斥操作需要加分布式鎖同步。 使用線程池、并發(fā)庫、并發(fā)類、同步工具而不是線程對象、并發(fā)原語。在復雜并發(fā)場景下,還需注意多個同步對象上的鎖是否按合適的順序獲得和釋放以避免死鎖,相應(yīng)的錯誤處理代碼是否合理。 事務(wù) 事務(wù)方面常出現(xiàn)的問題是:多個緊密關(guān)聯(lián)的業(yè)務(wù)操作和 SQL 語句沒有事務(wù)保證。 在資金業(yè)務(wù)操作或數(shù)據(jù)強一致性要求的業(yè)務(wù)操作中,要注意使用事務(wù),保證數(shù)據(jù)更新的一致性和完整性。 SQL問題 SQL的正確性通??梢酝ㄟ^ DAO 測試來保證。 SQL問題主要是指潛在的性能問題和安全問題。 要避免SQL性能問題, 在表設(shè)計的時候就要做好索引工作。在表數(shù)據(jù)量非常大的情況下,SQL語句編寫要非常小心。查詢SQL需要添加必要索引,添加合適的查詢條件和查詢順序,加快查詢效率, 避免慢查; 盡量避免使用 Join, 子查詢;避免SQL注入。 SQL優(yōu)秀書籍推薦: SQL語言藝術(shù)
安全問題 安全問題一向是互聯(lián)網(wǎng)產(chǎn)品研發(fā)中極容易被忽視、而在爆發(fā)后又極引發(fā)熱議的議題。安全和隱私是用戶的心理紅線之一。應(yīng)用、數(shù)據(jù)、資金的安全性應(yīng)當僅次于產(chǎn)品功能的準確性和使用體驗。 安全問題的CodeReview可參見檢查點清單:信息安全 。主要是如下措施: a. 嚴格檢查和屏蔽非法輸入; b. 對含敏感信息的請求加密通信; c. 業(yè)務(wù)處理后消除任何敏感私密信息的任何痕跡; d. 結(jié)果返回前在反序列化中清除敏感私密信息; e. 敏感私密信息在數(shù)據(jù)存儲設(shè)備中應(yīng)當加密存儲; f. 應(yīng)用有嚴格的角色、權(quán)限、操作、數(shù)據(jù)訪問分級和控制; g. 切忌暴露服務(wù)器的重要的安全性信息,防止服務(wù)器被攻擊影響正常服務(wù)運行。 設(shè)計問題 設(shè)計問題通常體現(xiàn)在: a. 是否有潛在的性能問題; b. 是否有安全問題; c. 業(yè)務(wù)變化時是否容易擴展; d. 是否有遺漏的點。 較輕微的問題 較輕微問題是指“沒有技術(shù)難度、通過良好習慣即可避免的問題”。 較輕微問題一般不會造成負面影響的BUG或故障,不過建立一些好的習慣,主動使用代碼檢測工具,消除這些較輕微錯誤,也是一種修行。 命名不貼切 命名不貼切不會影響功能實現(xiàn),卻會誤導理解或增加理解難度。 方法:先查查字典,找個通俗易懂而且比較貼近的名字??梢詤⒖?jdk 的命名、通用詞匯和行業(yè)詞匯; 作用域小的采用短命名,作用域大的采用長命名。取名字是一種重要技能,—— 多少父母為此愁灰了頭! 聲明時未初始化 聲明時未初始化通常情況下都不會是問題,因為后面會進行賦值。不過,如果賦值的過程中出現(xiàn)異常,那么可能會返回空值,從而導致空值異常。通常,變量聲明時賦予默認初始值是個好習慣。 風格與整體有不一致 工程通常求穩(wěn),一致性能更好地維護。在工程項目中,最好能夠遵循工程約定的風格,在個人項目中可以凸顯個性風格。Java編程一般要遵循《Java編程規(guī)范》,有追求的程序猿媛還會追求更高層次的,比如《Google Java 規(guī)范》等。 類型轉(zhuǎn)換錯誤 編程語言的類型系統(tǒng)是非常重要的。如何在不同類型之間可靠地互轉(zhuǎn),尤其是在父子類型之間相互賦值,也是一個微技能。濫用類型轉(zhuǎn)換,也會導致BUG 。 Java 中容易出現(xiàn)的錯誤是:a. 字符串轉(zhuǎn)數(shù)值,字符串含有非數(shù)字部分;b. JSON字符串轉(zhuǎn)對象,某個字段含有不兼容的值類型導致解析出錯;c. 子類型轉(zhuǎn)不兼容的父類型,滋生運行時異常 ClassCastException;d. 相同特質(zhì)的類型不兼容。比如 Long 與 Integer 都是數(shù)值型,卻不能互轉(zhuǎn)。 類型轉(zhuǎn)換中最容易出BUG的地方是非布爾類型取反。受C語言的影響,很多高級語言支持各種數(shù)據(jù)類型轉(zhuǎn)布爾類型,比如 PHP 字符串、數(shù)組、數(shù)字等都可以轉(zhuǎn)布爾類型,相應(yīng)的就喜歡寫 if (!notBoolVar) 這種表達式, 容易隱藏看不出的BUG甚至錯誤。 否定式風格 變量含義、表達式語句傾向于使用否定式風格,可能不知不覺耗費大量腦細胞,因為每次理解的時候都要繞個彎子。 比如 isNoExpress 是否無需物流, 就有點繞。 為什么呢? 無需物流是針對快遞發(fā)貨的, 如果快遞發(fā)貨占發(fā)貨的90%, 無需物流只占10%,那么, isNoExpress = false 幾乎總為真。 涉及到判斷的時候,可能不得不寫 if (!isNoExpress) , 雙重否定足夠弄暈?zāi)恪?/p> 容器遍歷的結(jié)構(gòu)變更 絕大多數(shù)語言都承襲了 C 語言的 for(int i=0;i API參數(shù)傳遞錯誤 如果API參數(shù)有多個,而且相鄰參數(shù)的類型相同,那么要特別留意是否參數(shù)順序是正確的,而不會張冠李戴。 當然,在設(shè)計API參數(shù)的時候,就可以仔細用更精準類型進行區(qū)分,并將相同類型的參數(shù)錯開。比如 calc(int accountNo, int pay, int timestamp) , 就容易傳錯,比較可靠的是 calc(int accountNo, Currency pay, Timestamp now) ,這樣是不可能將參數(shù)傳遞錯誤的。 單行調(diào)用括號過多 為了簡便,常常會寫出 wapper(calc(now, String.format(“%s\n”, new BufferedFileReader(filename, “UTF-8″).readLines() ))) 的語句 , 嗯,你得好好瞧瞧和算算右邊的括號數(shù)量是否正確了。更糟糕的時候,結(jié)合API參數(shù)傳遞錯誤,IDE 可能沒有報錯, 而你很可能沒有意識到自己的參數(shù)傳遞錯誤了。 可靠的方式是, 拆出一部分變量,并將調(diào)用之間的括號用空格隔開,顯示出層次感。
修改方法簽名 對某個方法有業(yè)務(wù)改動時,程序猿媛們傾向直接修改原方法的簽名。這時,要特別注意:a. 不要修改原方法的參數(shù)順序; b. 在最后面增加可選參數(shù)。 從另一個角度來看,復雜的業(yè)務(wù)方法應(yīng)當分兩層: 最外層負責調(diào)度,方法參數(shù)具有包容性,里面包含的字段比較多 ; 內(nèi)層方法負責特定業(yè)務(wù)邏輯的實現(xiàn),方法參數(shù)少而精。 修改原方法簽名本身就是容易產(chǎn)生問題的習慣, 篡改原方法的參數(shù)順序更是大忌。 最好的方法是新建一個方法去復用原方法, 然后調(diào)用新的方法。代碼變更始終銘記“開閉”原則。 打印日志太多 打印過多的日志并不好。一方面遮掩真正需要的信息,導致排查耗費時間, 另一方面造成服務(wù)器空間浪費、影響性能。生產(chǎn)環(huán)境日志一般只開放 INFO及以上級別的日志; Debug 日志只在調(diào)試或排錯的時候使用,生產(chǎn)環(huán)境可以禁止debug日志。 多級數(shù)據(jù)結(jié)構(gòu) 使用多級數(shù)據(jù)結(jié)構(gòu)時,要確定父級數(shù)據(jù)一定有值,或者進行檢測。比如 $order['baole']['ump']['money'],必須確保 $order['baole'], $order['baole']['money'] 一定有值或做非空檢測。 作用域過大 由于C語言的影響,猿媛們會在開頭就定義好一些變量或要返回的對象,在很靠后的地方才使用到。不必要的過大的作用域?qū)ψ兞亢蛯ο蟮淖兓a(chǎn)生不可測的影響,并增大理解的成本。可靠的方法是,僅當在使用時才定義,并盡快返回結(jié)果。 另一種情況是,暴露的訪問域過大,比如 public 字段。 盡可能地縮小可訪問的范圍,可以增大變更和重構(gòu)的空間; 減少可變性,則可以自然地獲得并發(fā)安全性,降低CodeReview的理解成本。 比如,不可變的類和字段定義成 final , 最小化包,類,接口,方法和域的可訪問性,默認為 private , 若需要繼承,可定義為 protected , 僅當需要作為 API 服務(wù)暴露出去時,使用 public. 分支與循環(huán) 條件與循環(huán)偶爾也會導致錯誤, 不過通常錯誤可以在發(fā)布前解決掉。 對于 if-else 嵌套條件, 需要仔細檢查是否符合業(yè)務(wù)邏輯; 如果嵌套太深,是否可以使用另一種方式“解結(jié)” ; 對于 switch 語句, 大多數(shù)語言的 case 有 fall through 問題, 要注意加上 break ; 最好加上 default 的處理。 對于 for 循環(huán), 編寫合理的結(jié)束條件避免死循環(huán); 對于循環(huán)變量的控制, 避免出現(xiàn) -1或 +1 錯誤, 消除越界錯誤; for 循環(huán)也要特別注意對空值和空容器的處理,避免拋出空值異常??梢酝ㄟ^單測來確保 for 循環(huán)的準確性。 看完本文有收獲?請轉(zhuǎn)發(fā)分享給更多人 |
|