驚了!同事竟然在代碼里“下毒”
原創(chuàng)【51CTO.com原創(chuàng)稿件】寫這個文章是因為前段時間確實因為公司的業(yè)務開發(fā)太忙太緊,所有開發(fā)都處在于加班趕項目,并且加入的新人較多造成了一系列代碼不可控的質量問題。
圖片來自 Pexels
文章針對這段時間代碼出現(xiàn)的各種各樣的問題進行了一個概況和整理,主要集中在代碼編碼的問題,抽象化的問題,還有就是涉及到微服務中調用和編寫接口的問題。
其實按道理來說,這些應該屬于編程的基本功,貌似不太值得寫一篇文章,不過倒是可以通過這些基本功的出發(fā),去討論一個代碼編程系統(tǒng)構建的一個本質,所以還是比較值得去展開。
大概先鋪墊下,會按照一個原則和建議來展開一個一個的進行討論。
編碼的問題
避免過多的 IF 嵌套
所謂的“箭頭形”代碼基本都是因為大量的 IF 嵌套導致,一方面形成一個深深的箭頭形狀,在閱讀代碼造成縮進夸張的語句塊。
更要命的是過深的嵌套層次導致代碼邏輯復雜度加深,當閱讀到第 N 層嵌套時根本不清楚是什么邏輯才能進入,嚴重降低代碼的可閱讀性和可維護性。
???
其實對應 IF-ELSE 過長的主要原因無非就是對當前狀態(tài)進行檢查并決定繼續(xù)還是跳轉。
①使用衛(wèi)語句(Guard Clauses)提前返回,避免層層嵌套
先對 IF/ELSE 的邏輯結構進行一些分析,我們基本上有兩種用法:
"優(yōu)先考慮滿足條件,進行處理流程",代碼如下:
if(user.getId() == 10){ //滿足條件,執(zhí)行 }else{ //不滿足條件,退出 }
“優(yōu)先考慮不滿足條件,讓其邏輯退出流程”,代碼如下:
if(user.getId() != 10){ //不滿足條件,退出 }else{ //滿足條件,執(zhí)行 }
這是兩個不同的邏輯結構,他們都可以寫出同樣的代碼邏輯,但是在第一種中,如果代碼量增大,嵌套增多,就很容易在條件中迷失了方向。
如果采用第二種方式把條件反過來寫,盡早的能把退出型邏輯及早的退出,這樣就可以把箭頭型的代碼解脫掉,如下圖:
???
②規(guī)劃好判斷條件和狀態(tài)模型
代碼如下圖:
???
如果是業(yè)務允許其實是可以將多個判斷條件進行整合,這樣可以避免箭頭形代碼的出現(xiàn)。
但是僅僅一段 IF 條件判斷的語句又變得非常的臃腫一行都放不下,如果出現(xiàn)了非常復雜多狀態(tài)判斷和組合,可以使用“狀態(tài)表”,或者是狀態(tài)機等設計模式來進行解耦。
③將 IF 中的業(yè)務細節(jié)進行抽象成函數(shù)
將 IF 中繁瑣的業(yè)務細節(jié)抽成函數(shù),一方面可以減少又長又臭的代碼,更利于屏蔽細節(jié),將不關流程的業(yè)務邏輯鎖定在一個特定的區(qū)域。
也利于進行代碼閱讀,讓閱讀關注于業(yè)務的流程而不是業(yè)務實現(xiàn)的細節(jié),要善于應用函數(shù)用于代碼的封裝和抽象。
???
謹慎多層循環(huán)嵌套中的操作
有的時候,確實幾層 for 循環(huán)的嵌套是業(yè)務實現(xiàn)的必須,但我們需要警惕的是經(jīng)過幾層循環(huán)的放大,最內層循環(huán)執(zhí)行的數(shù)量是多層循環(huán)數(shù)量的乘積。
???
例如,這段代碼總共經(jīng)歷了 4 層的循環(huán),如果循環(huán)是 10x10x10x10,那么最終的 DB 操作是要經(jīng)歷單獨的開銷 10000 次。
第一,這 10000 次開銷如果是程序員在寫代碼已經(jīng)明確知道的開銷屬于業(yè)務必須那倒無妨,只怕程序員在寫代碼的時候還無意識到這個點是會被隨時放大。
第二,即使 10000 次開銷是屬于業(yè)務必須,那按照這個代碼來看,還是存在可以優(yōu)化的空間,可以在循環(huán)中將所有查詢條件都進行拼湊,然后在進行一定程度的批量查詢,可以較大程度降低 DB 的開銷。
不要隨意定義局部變量名
命名風格我們可以參考阿里的《Java開發(fā)手冊》,這里主要指出來的是局部變量隨意命名的現(xiàn)象比較嚴重,大家一般都會以為局部變量只是在本方法內使用,又不會對其他方法和其他人造成影響。
但殊不知局部變量名起得不好或隨意也對開發(fā)者本身造成困擾甚至連自己到不知道的錯誤,以下是一個比較經(jīng)典的隨意起變量名的例子:
???
變量名 ma 和 map 沒有本身含義,并且他們的泛類又是一樣,很難保證不會再下面的代碼不小心使用錯誤。
避免又臭又長的類和方法
一點都不夸張,之前看到過一類一千多行,一個方法長達 300 行,IDE 大概一頁正常來說 30-50 行(取決屏幕大小),這個叫閱讀者怎么查看。
閱讀的時候,不斷的滾輪翻頁,就算是原作者,恐怕時間一長也很難駕馭這個類,就不用說后來的維護者了。
更重要的是一個類,一個方法過長時,會嚴重阻礙你的擴展和修改,方法中每一個邏輯都牽扯到很多分散的上下文,會讓修改和擴展異常困難。
按照《重構》所說,出現(xiàn)類過長的情況很多是職責不明確,一個類存在著幾十個方法,那絕對是職責過多或職責不細分。
簡單列一下針對又長又臭的重構處理:
- 分析需要重構類的功能。
- 將職責相同的方法使用組合或集成的方式抽取為獨立的類。
- 分析各個方法,將重復的代碼提取為函數(shù)。
- 命名,對類有一個好的命名有利于對類的定位和確立職責。
Log 日志要提供明確的指向,輔助定位
Log 日志要有明確的指向性,一個可以輔助調試,一個可以記錄事件,和確立定位錯誤。
像以下的這個例子,打印了一個 log.error 日志,但這個錯誤,就算我們事后去查看日志,只知道這里有一個錯誤日志,但究竟是哪一個用戶日志,哪一張優(yōu)惠券的日志,無從得知,不能有助于我們直接定位錯誤。
???
再看一下的日志,將返回的一個 List 進行直接打印,此處的打印并無助于保留和定位問題,只會留下無價值的信息并且讓日志變得亂糟糟。
通常,我們留下實體名字和邏輯關鍵字就足以識別一條記錄。
???
復雜模塊,代碼未動,大綱注釋先行
要阻止一個初級的程序員一上來就寫代碼的難度堪比阻止一饑餓的人要飽餐一頓,有多少程序員被稱之為碼農,一上來就想搬磚。
在流程和系統(tǒng)的設計上,我們有 E-R 圖和流程圖,幫我們建立模型和流程。
當我們碰到邏輯比較復雜的類或方法,我們也需要先梳理好邏輯和流程,用注釋或偽代碼定好邏輯和流程,把整體的思路確立后,搭起一個骨架,再往里面填肉(寫代碼)。
只要流程清晰,邏輯明朗,這個時候寫代碼其實是最簡單的事情。
???
功能相同盡量抽象,不要發(fā)散式修改
舉這次我們構建訂單的一個例子,見下圖:
???
下單在后端使用了適配者的一個設計模式,主要是包裝同一個接口對外暴露,然后根據(jù)情況(商品的邏輯)進行實現(xiàn)類的分離。
把邏輯統(tǒng)一并包裝成統(tǒng)一接口對外暴露這個本意是良好的,但是在這里例子中,只在意了商品邏輯的分離,而忽略了,其實邏輯,例如庫存,支付,優(yōu)惠券等邏輯其實是統(tǒng)一的,是可以被抽象的。
導致的結果是例如需要修改優(yōu)惠券邏輯式,需要同時進行三次幾乎一模一樣的修改。
可以從上圖看出來,過早的使用適配模式,將業(yè)務在入口處進行分離,導致了后續(xù)其實相同邏輯的業(yè)務代碼也進行了分離,本來 “扣庫存” “扣優(yōu)惠券” “支付”等邏輯應該是一樣,但也使用了三套代碼進行維護。
微服務編碼問題
RPC 接口必須是業(yè)務職責
RPC 接口是微服務的生產(chǎn)者提供一定的能力給到消費者進行使用,這個時候的 RPC 接口千萬不要定義大而全的接口。
之前就發(fā)現(xiàn)有部分同學把 RPC 接口定義成:
insertXXX updateXXX listXXX
這樣無異于把 DAO 層直接搬到了 RPC,把整個 DAO 直接進行暴露,這樣違背了微服務的接口調用原則,RPC 接口只提供最原子的功能,限制消費者在生產(chǎn)者定義好的業(yè)務中進行使用。
嚴禁循環(huán)調用 RPC 接口
與項目內編程不同的是,每個 RPC 接口的調用都會伴隨著一次的網(wǎng)絡開銷,需要需要對一個接口進行反復請求,這個時候可以要求 RPC 接口的提供方另外提供一個可以批量的接口,將單次反復的請求變成一次請求,減少網(wǎng)絡開銷。
???
使用工具輔助清理惡性代碼
P3C 插件
在使用 Eclipse 或 idea 編程中,首推使用阿里的 P3C 插件進行輔助,代碼規(guī)范檢查插件 P3C,是根據(jù)《阿里巴巴Java開發(fā)手冊》轉化而成的自動化插件。
使用 Skywalking 找出惡性代碼
與 P3C 直接輔助編碼不同的是,Skywalking可以在生產(chǎn)環(huán)境中通過鏈路的跟蹤確定某一個微服務的接口性能或調動出現(xiàn)異常。
這里不累贅介紹 Skywalking 的用處,其實鏈路跟蹤不僅僅是運維或架構師應該關注的點,普通的開發(fā)者也可以借助鏈路跟蹤去回溯自己的代碼,站在一個高的角度在生產(chǎn)環(huán)境中審視代碼在鏈路中表現(xiàn)。
善于使用鏈路跟蹤往往可以發(fā)現(xiàn)在平時編碼中被忽略的問題,例如,一次不經(jīng)意的循環(huán)調用 RPC 很容易就造成超大的調用跨度,而往往在編程中開發(fā)者是未能及時感知的。
???
小結
在分享的時候其實還講了抽象的原則和一些設計模式的使用,這里就不累贅的復述了。
簡單的說,要寫出好的性能,可讀性高,邏輯明了的代碼,往往靠的不是一次一次的 CURD,而是平時的總結和思考。
作者:陳于喆
簡介:十余年的開發(fā)和架構經(jīng)驗,國內較早一批微服務開發(fā)實施者。曾任職國內互聯(lián)網(wǎng)公司網(wǎng)易和唯品會高級研發(fā)工程師,后在創(chuàng)業(yè)公司擔任技術總監(jiān)/架構師。
編輯:陶家龍
征稿:有投稿、尋求報道意向技術人請聯(lián)絡 editor@51cto.com
【51CTO原創(chuàng)稿件,合作站點轉載請注明原文作者和出處為51CTO.com】
???