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