聊聊那些年遇到過(guò)的奇葩代碼
引言
無(wú)論是開(kāi)發(fā)新需求還是維護(hù)舊平臺(tái),在工作的過(guò)程中我們都會(huì)接觸到各種樣式的代碼,有時(shí)候會(huì)碰到一些優(yōu)秀的代碼心中不免肅然起敬,但是更多的時(shí)候我們會(huì)遇到很多奇葩代碼,有的時(shí)候罵罵咧咧的吐槽一段奇葩代碼后定睛一看作者,居然是幾個(gè)月以前自己的寫(xiě)的,心中難免浮現(xiàn)曹操的那句名言:不可能,絕對(duì)不可能。很多同學(xué)可能會(huì)說(shuō)要求別太高了,代碼能跑就行。但是實(shí)際上代碼就是程序猿的名片,技術(shù)同學(xué)不能局限于實(shí)現(xiàn)功能需求,還是得有寫(xiě)高質(zhì)量代碼的追求。那么今天就和大家聊聊那些年遇到過(guò)的奇葩代碼,看看自己以前有沒(méi)有寫(xiě)過(guò)這樣的代碼,現(xiàn)在還會(huì)不會(huì)這樣寫(xiě)了。
奇葩代碼大賞
命名沒(méi)有業(yè)務(wù)語(yǔ)義
可能乍一看這段代碼其實(shí)沒(méi)啥大問(wèn)題,但是如果要知道這段代碼到底是干嘛的可能你一下子反應(yīng)不過(guò)來(lái),需要好好看看代碼邏輯才知道。通過(guò)查看代碼我們知道此處的代碼業(yè)務(wù)語(yǔ)義是變更任務(wù)狀態(tài),但是實(shí)際的方法名稱是handleTask,命名明顯過(guò)于寬泛了,不能精確表達(dá)實(shí)際的業(yè)務(wù)語(yǔ)義。
那么為什么要把代碼擼一遍才能明確方法的含義呢?歸根到底就是方法命名不夠準(zhǔn)確,不能完全表達(dá)這段代碼所對(duì)應(yīng)的業(yè)務(wù)語(yǔ)義。那為什么我們經(jīng)常不能很準(zhǔn)確的進(jìn)行類或者方法的命名呢?我想最根本的原因還是碼代碼的同學(xué)沒(méi)能夠精準(zhǔn)把握這段代碼的業(yè)務(wù)語(yǔ)義,因此在起名字的時(shí)候要么過(guò)于寬泛,要么詞不達(dá)意。
因此無(wú)論是類命名或者方法命名都要能夠明確的表達(dá)業(yè)務(wù)語(yǔ)義,只有這樣無(wú)論是一段時(shí)間自己回過(guò)頭來(lái)看或者其他維護(hù)者來(lái)看代碼都能夠通過(guò)看命名就可以明確代碼蘊(yùn)含的業(yè)務(wù)邏輯。
單個(gè)方法過(guò)長(zhǎng)
特別是在一些老項(xiàng)目中,我們經(jīng)常會(huì)遇到一個(gè)方法里面能塞進(jìn)去幾百行代碼。一般造成這種單個(gè)方法代碼過(guò)長(zhǎng)的原因無(wú)非有兩個(gè),一個(gè)是用過(guò)程化的思維編寫(xiě)代碼,想到哪些業(yè)務(wù)步驟都統(tǒng)統(tǒng)寫(xiě)在一個(gè)方法中;另一個(gè)就是后來(lái)的維護(hù)者需要增加新的功能,一看代碼這么長(zhǎng)也不敢瞎改只能在長(zhǎng)方法中繼續(xù)碼代碼,造成方法原來(lái)越長(zhǎng)難以維護(hù)。
無(wú)論是從后期代碼可維護(hù)性還是從SRP設(shè)計(jì)原則來(lái)說(shuō),單個(gè)方法中代碼行數(shù)最好不要超過(guò)100行,否則帶來(lái)的后果就是各種業(yè)務(wù)邏輯糅合在一起,不僅后期維護(hù)代碼的同學(xué)不容易理解其中包含的業(yè)務(wù)語(yǔ)義,而且如果功能變化修改起來(lái)也比較費(fèi)勁。
如上架鮮品的邏輯,可以看的出來(lái)在上架生鮮產(chǎn)品的時(shí)候會(huì)經(jīng)歷貨品檢查、貨品擺渡、貨品上架等多個(gè)個(gè)步驟,但是在這個(gè)shelveFreshGoods方法中將這些業(yè)務(wù)步驟走雜糅在了一起,如果我們想修改或者增加業(yè)務(wù)邏輯的時(shí)候就需要在這個(gè)方法中只能在這個(gè)長(zhǎng)方法中進(jìn)行修改,可能會(huì)導(dǎo)致方法越來(lái)越長(zhǎng)。而如果通過(guò)拆分的方式進(jìn)行業(yè)務(wù)子過(guò)程劃分,也就是說(shuō)將上述的幾個(gè)步驟都封裝成方法。那么修改某業(yè)務(wù)邏輯可直接在對(duì)應(yīng)拆出來(lái)的步驟中進(jìn)行,這樣修改的范圍就縮小了,另外業(yè)務(wù)邏輯看上去一目了然。
業(yè)務(wù)數(shù)據(jù)循環(huán)插入
在進(jìn)行業(yè)務(wù)代碼開(kāi)發(fā)的時(shí)候,批量進(jìn)行業(yè)務(wù)數(shù)據(jù)插入是非常常見(jiàn)的CRUD基操。但是有的同學(xué)在寫(xiě)批量插入接口的時(shí)候會(huì)這么寫(xiě),通過(guò)for循環(huán)或者stream來(lái)進(jìn)行循環(huán)數(shù)據(jù)寫(xiě)入。這樣的寫(xiě)法會(huì)平白增加服務(wù)與數(shù)據(jù)庫(kù)的交互次數(shù),占用不必要的數(shù)據(jù)庫(kù)連接,很容易遇到性能問(wèn)題。如果一次性插入的數(shù)據(jù)不多的話(幾條數(shù)據(jù))倒也影響不大,但是如果數(shù)據(jù)量多起來(lái)的話必定會(huì)成為性能瓶頸。
很明顯可以看得出來(lái),原先的寫(xiě)法需要與數(shù)據(jù)庫(kù)進(jìn)行多次交互。而優(yōu)化后的寫(xiě)法只需要和數(shù)據(jù)庫(kù)交互一次。實(shí)際上我們可以在mapper文件中進(jìn)行批量插入進(jìn)行優(yōu)化,這樣實(shí)際上通過(guò)批量插入的sql語(yǔ)句,從而實(shí)現(xiàn)服務(wù)與數(shù)據(jù)庫(kù)只交互一次就完成數(shù)據(jù)的批量保存。
先查數(shù)據(jù)再更新數(shù)據(jù)庫(kù)
在進(jìn)行業(yè)務(wù)代碼編寫(xiě)的時(shí)候,經(jīng)常會(huì)碰到這樣的場(chǎng)景,如果數(shù)據(jù)庫(kù)中有數(shù)據(jù)則進(jìn)行更新,如果沒(méi)有數(shù)據(jù)則直接插入。我們來(lái)看看下面這種寫(xiě)法,先從數(shù)據(jù)庫(kù)中查詢數(shù)據(jù),如果存在則進(jìn)行更新,如果不存在則進(jìn)行數(shù)據(jù)插入,有兩次數(shù)據(jù)庫(kù)交互操作。
實(shí)際上可以直接通過(guò)數(shù)據(jù)庫(kù)的sql進(jìn)行控制,存在數(shù)據(jù)則進(jìn)行更新,不存在則插入,這樣可以避免和數(shù)據(jù)庫(kù)的多次交互。
業(yè)務(wù)依賴技術(shù)細(xì)節(jié)
我們先來(lái)看下Robert C. Martin提出來(lái)的依賴倒置原則怎么描述的:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
高層模塊不應(yīng)該依賴于低層模塊,二者都應(yīng)該依賴于抽象。
- Robert C. Martin
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
抽象不應(yīng)該依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴于抽象。
- Robert C. Martin
這兩句話聽(tīng)上去有點(diǎn)不明覺(jué)厲,不如我們結(jié)合下具體的業(yè)務(wù)場(chǎng)景更好理解一點(diǎn)。假設(shè)在一個(gè)監(jiān)控告警平臺(tái)中,如果線上平臺(tái)出現(xiàn)了問(wèn)題,比如調(diào)用訂單生成接口失敗,無(wú)法生成訂單。平臺(tái)檢測(cè)到這樣的異常之后需要通知研發(fā)同學(xué)進(jìn)行問(wèn)題排查定位。此時(shí)監(jiān)控告警平臺(tái)會(huì)將告警信息發(fā)送到釘釘群中進(jìn)行通知。因此我們需要一個(gè)發(fā)送釘釘消息的接口,如下所示。
看上去代碼是沒(méi)什么問(wèn)題的,有了告警就調(diào)用發(fā)送釘釘消息的接口方法。但是實(shí)際上這樣的寫(xiě)法違反了依賴倒置的設(shè)計(jì)原則。為什么這么說(shuō),試想一下如果哪天公司決定不用釘釘接收告警信息,改用企業(yè)微信了或者是自己公司的通訊軟件。那么此處的sendDingTalk必定是要進(jìn)行修改的,因?yàn)槲覀兊母婢ㄖ獦I(yè)務(wù)依賴了具體的發(fā)送消息通知的實(shí)現(xiàn)細(xì)節(jié),這明顯是不合理的。
因此此處比較好的做法是,定義一個(gè)notifyMessage的接口,具體的實(shí)現(xiàn)細(xì)節(jié)上層不必關(guān)心,無(wú)論是通過(guò)釘釘通知還是企業(yè)微信通知也好,只要實(shí)現(xiàn)這個(gè)通知的接口就OK了。即便后期進(jìn)行切換,原來(lái)的業(yè)務(wù)邏輯并不需要進(jìn)行修改,只要修改具體通知接口的實(shí)現(xiàn)就可以了。
長(zhǎng)SQL
程序猿接手項(xiàng)目的時(shí)候,最怕遇到的就是項(xiàng)目中那些動(dòng)不動(dòng)上百行的長(zhǎng)SQL。這些長(zhǎng)SQL中有的存在各種嵌套查詢,甚至包含了三四層子查詢;有的包含了四五個(gè)left join,inner join連接了五六張表,這些長(zhǎng)SQL一個(gè)電腦屏幕都裝不下,仿佛裝不下的還有寫(xiě)這個(gè)長(zhǎng)SQL的同學(xué)的“才華”。更無(wú)語(yǔ)的是如果寫(xiě)這個(gè)SQL的同學(xué)已經(jīng)離職了,你想問(wèn)下大致的查詢邏輯都沒(méi)人可以問(wèn),即便是沒(méi)有離職,寫(xiě)的人過(guò)了一段時(shí)間后再看這段SQL估計(jì)也挺費(fèi)勁。
可能有的同學(xué)會(huì)說(shuō)我也不想寫(xiě)長(zhǎng)SQL啊,奈何數(shù)據(jù)分散在各個(gè)表中,業(yè)務(wù)邏輯也比較復(fù)雜,所以只能各種join各種子查詢,不知不覺(jué)就寫(xiě)了長(zhǎng)SQL。但是實(shí)際上長(zhǎng)SQL并不能解決上述數(shù)據(jù)分散業(yè)務(wù)復(fù)雜的問(wèn)題,反而帶來(lái)了后期維護(hù)差等各種問(wèn)題,長(zhǎng)SQL表面上看是一個(gè)數(shù)據(jù)庫(kù)操作,但是在數(shù)據(jù)庫(kù)引擎層面還是將長(zhǎng)SQL分成了多個(gè)子操作,各個(gè)子操作完成后再將結(jié)果數(shù)據(jù)進(jìn)行統(tǒng)一返回。
那么如何避免寫(xiě)出來(lái)這種維護(hù)性很差的長(zhǎng)SQL呢?對(duì)于一些查詢場(chǎng)景比較多的長(zhǎng)SQL可以嘗試使用大寬表來(lái)承載需要展示的各個(gè)字段數(shù)據(jù),這樣頁(yè)面查詢的時(shí)候直接在大寬表上進(jìn)行查詢,而不必再組合各個(gè)業(yè)務(wù)數(shù)據(jù)進(jìn)行查詢,或者將又有的長(zhǎng)SQL拆分成多個(gè)視圖以及存儲(chǔ)過(guò)程來(lái)簡(jiǎn)化SQL的復(fù)雜性。
接口參數(shù)過(guò)多
這個(gè)問(wèn)題在實(shí)際項(xiàng)目開(kāi)發(fā)中經(jīng)常遇到,當(dāng)你需要調(diào)一個(gè)別人封裝好的接口的時(shí)候,對(duì)方突然丟過(guò)來(lái)一個(gè)方法包含了七八個(gè)參數(shù)。我想當(dāng)時(shí)你的心情應(yīng)該是想對(duì)他深深說(shuō)一句真是栓Q你了。其實(shí)對(duì)于一個(gè)方法的參數(shù)來(lái)說(shuō),這里建議參數(shù)個(gè)數(shù)還是最多不要超過(guò)5個(gè)。
實(shí)際上我們可以用模型對(duì)象來(lái)進(jìn)行參數(shù)封裝,這樣可以避免方法中參數(shù)個(gè)數(shù)過(guò)多導(dǎo)致后期維護(hù)困難。因?yàn)殡S著業(yè)務(wù)的發(fā)展,有可能會(huì)出現(xiàn)修改接口能力來(lái)滿足新的需求,但是這個(gè)時(shí)候如果動(dòng)接口參數(shù)的話,那么對(duì)應(yīng)的接口以及實(shí)現(xiàn)類都需要修改,萬(wàn)一有其他地方調(diào)用這個(gè)接口,那么修改的地方就會(huì)更多,很明顯這不符合OCP設(shè)計(jì)原則。因此這個(gè)時(shí)候如果使用的是一個(gè)對(duì)象作為方法的參數(shù),那么無(wú)論是增加或者減少參數(shù)都只需要修改參數(shù)對(duì)象,并不需要修改對(duì)應(yīng)方法的接口參數(shù),這樣接口的擴(kuò)展性會(huì)更加強(qiáng)一點(diǎn)。因此我們?cè)趯?xiě)代碼的時(shí)候不能光著眼于當(dāng)下,還要考慮對(duì)應(yīng)需求發(fā)生變化的時(shí)候,我的代碼怎么才能適應(yīng)這種變化做到最小化修改,后期無(wú)論是自己維護(hù)還是別人的同學(xué)維護(hù)都會(huì)更加方便一點(diǎn)。
重復(fù)代碼
之前專門(mén)寫(xiě)過(guò)關(guān)于如何消除系統(tǒng)重復(fù)的代碼的文章,具體可以參見(jiàn)如下:
如何優(yōu)雅的消除系統(tǒng)重復(fù)代碼
常見(jiàn)代碼優(yōu)化寫(xiě)法
盡量復(fù)用工具函數(shù)
集合判斷
日常開(kāi)發(fā)的時(shí)候我們經(jīng)常遇到關(guān)于數(shù)據(jù)集合非空判斷的邏輯,常見(jiàn)的寫(xiě)法如下,雖然沒(méi)什么問(wèn)題但是看起來(lái)非常不順溜,簡(jiǎn)單來(lái)說(shuō)就是不夠直接,一眼望過(guò)去還得反應(yīng)一下。
但是通過(guò)使用封裝好的工具類直接進(jìn)行判斷,所看即所得,清楚明白表達(dá)集合檢查邏輯。
Boolean轉(zhuǎn)換
在一些場(chǎng)景下我們需要將Boolean值轉(zhuǎn)化為1或者0,因此常見(jiàn)如下代碼:
實(shí)際上可以借助于工具方法簡(jiǎn)化為如下代碼:
lambda表達(dá)式簡(jiǎn)化集合
集合最常見(jiàn)的場(chǎng)景就是進(jìn)行數(shù)據(jù)過(guò)濾,篩選出符合條件的對(duì)象,代碼如下:
實(shí)際上我們可以利用lambda表達(dá)式進(jìn)行代碼簡(jiǎn)化:
Optional減少if判斷
假設(shè)我們要獲取任務(wù)的名稱,如果沒(méi)有則返回unDefined,傳統(tǒng)的寫(xiě)法可能是這樣,包含了多個(gè)if判斷,看上去有點(diǎn)啰里啰唆不夠簡(jiǎn)潔。
我們嘗試使用Optional進(jìn)行代碼簡(jiǎn)化優(yōu)化之后,是不是看上去立馬簡(jiǎn)潔很多了?
總結(jié)
本文主要和大家聊了聊日常工作中比較常見(jiàn)的奇葩代碼,當(dāng)然吐槽并不是目的,研發(fā)同學(xué)能夠識(shí)別到奇葩代碼并進(jìn)行優(yōu)化,同時(shí)自己在實(shí)際開(kāi)發(fā)工程中能夠盡量避免寫(xiě)這些代碼才是真正的目的。不知道大家在工作中有沒(méi)有遇到過(guò)類似的奇葩代碼或者自己曾經(jīng)寫(xiě)過(guò)哪些現(xiàn)在回過(guò)頭來(lái)看比較奇葩的代碼,如果有的話歡迎大家在評(píng)論區(qū)一起討論交流哈 。