一戈擒、不放過細節(jié)
很多的隱藏很深的bug就是代碼里面一些小細節(jié)不注意導致的券躁,比如一個變量被定義的位置,或者一個自己不了解細節(jié)的api調(diào)用等谒主。很多細節(jié)的處理也決定了代碼質(zhì)量朝扼,比如命名、代碼規(guī)范的處理等霎肯。
我們看下面這段代碼:
private void reduceValueConvert(CouponCategoryDTO dto) {
BigDecimal reduceValue = BigDecimal.ZERO;
if (MarketConstant.CategoryType.ZKQ.equals(dto.getType()) || MarketConstant.CategoryType.SPQ.equals(dto.getType())) {
reduceValue = dto.getReduceValue().multiply(new BigDecimal(10)).subtract(new BigDecimal(10)).abs();
}
if (MarketConstant.CategoryType.SPQ.equals(dto.getType()) && MarketConstant.DiscountType.FPQ.equals(dto.getDiscountType())) {
reduceValue = dto.getReduceValue();
}
dto.setReduceValue(reduceValue);
}
這是我前段時間在排查問題的時候看到的代碼擎颖,剛好這段代碼的細節(jié)問題非常多。這里先簡單介紹一下這個函數(shù)的目的:根據(jù)傳入的CouponCategoryDTO
判斷優(yōu)惠券是商品券還是折扣券观游,如果是商品券MarketConstant.CategoryType.SPQ
搂捧,則不做轉(zhuǎn)換,如果是MarketConstant.CategoryType.ZKQ
懂缕,則將傳入的dto里面的reduceValue
設(shè)置成供頁面顯示的X折這種形式允跑。邏輯非常簡單,但代碼看上去卻差點意思搪柑,我們來剖析一下看看:
命名
這個方法名叫reduceValueConvert
聋丝,從字面意思上理解的話,可以理解為“轉(zhuǎn)換減少值”工碾,應該是針對傳入的dto對象中的reduceValue做轉(zhuǎn)換的弱睦。但是具體要做什么樣的轉(zhuǎn)換,以及為什么要轉(zhuǎn)換渊额,單從方法名上是看不出來的况木,那假設(shè)換一種命名方式叫做:getDisplayReduceValue
,這時候理解起來就知道旬迹,ok火惊,這是因為reduceValue需要轉(zhuǎn)換成ui需要的顯示格式,所以定義一個函數(shù)專門用來干這件事情奔垦,方法名具體了很多矗晃,也更好理解了。
傳參
我們看之前這個方法傳了個CouponCategoryDTO
進去宴倍,雖然說這樣傳參減少了參數(shù)的數(shù)量张症,但是一旦傳入這個對象,這就是個引用傳遞鸵贬,那么在方法內(nèi)部是可以修改這個參數(shù)內(nèi)部屬性的數(shù)據(jù)的(這個函數(shù)確實這么干了??)俗他,對外部調(diào)用來說,如果后續(xù)依賴這個參數(shù)對象的話阔逼,不建議直接傳對象進方法兆衅,而是new一個新的對象,或者只傳需要的參數(shù)進入這個方法就可以了。
簡潔
我們看到羡亩,函數(shù)內(nèi)部有2個if判斷摩疑,而后面那個if判斷會修改前面那個if代碼塊里面的reduceValue返回值,閱讀起來還是比較啰嗦的畏铆,有簡化空間雷袋;對于靜態(tài)枚舉變量,可以使用static import簡化很多代碼辞居。
BUG
這個代碼里面是隱藏一個bug的楷怒,細心就會發(fā)現(xiàn),函數(shù)里面的2個if塊瓦灶,可能都沒有滿足鸠删,這種情況下,函數(shù)會將dto里面的reduceValue設(shè)置成BigDecimal.ZERO
贼陶,這里就引發(fā)了bug了刃泡。產(chǎn)生這個BUG的原因其實還是寫代碼在細節(jié)處理上的壞習慣:不要在函數(shù)內(nèi)部改變參數(shù)對象的屬性。
重構(gòu)后代碼
這里我對這段代碼進行了一些重構(gòu)碉怔,先看代碼:
private BigDecimal getDisplayReduceValue(BigDecimal originalReduceValue, Integer categoryType, Integer discountType) {
if (ZKQ.equals(categoryType) || SPQ.equals(categoryType)) {
if (!FPQ.equals(discountType)) {
return originalReduceValue.multiply(BigDecimal.TEN).subtract(BigDecimal.TEN).abs();
}
}
return originalReduceValue;
}
我們看到烘贴,重構(gòu)后的代碼,函數(shù)名非常具體眨层,就是去獲取顯示折扣庙楚,然后傳入的3個參數(shù)名也很具體上荡,使用了static import讓代碼相對簡潔一些趴樱。只在指定的if塊里面進行轉(zhuǎn)換,其它情況仍然返回originalReduceValue酪捡。代碼沒有改變?nèi)魏螀?shù)的值叁征,具體對返回值做什么處理,由調(diào)用該函數(shù)的使用者來決定逛薇。代碼看上去也比之前更容易維護了捺疼。
二、認真學習設(shè)計原則SOLID
SOLID原則是5大設(shè)計原則的首字母簡稱永罚,分別為:
單一職責原則 SRP
在任何一個軟件模塊中啤呼,應該有且只有一個被修改的原因。這里強調(diào)2個點:
1呢袱、職責要單一官扣,其它地方動了,不應該影響我羞福,我動了惕蹄,也不應該影響別人。
2、不能有多于一個職責卖陵,因為一旦職責多了遭顶,一個改動會影響另一個。
舉個實際場景的例子泪蔫,比如我有一個類棒旗,是叫OrderService
,那這里面應該都是和訂單相關(guān)的函數(shù)鸥滨,如果萬一出現(xiàn)一個支付的嗦哆、優(yōu)惠券的、購物車的婿滓,那就破壞了單一職責原則(其實這個例子不好老速,因為訂單服務職責也太多了,應該拆解為訂單查詢凸主,下單橘券,訂單支付等)卿吐。
開閉原則 OCP
軟件實體應該對擴展開放旁舰,對修改關(guān)閉嗡官。這里也強調(diào)2點:
- 有新需求時,可以對現(xiàn)有代碼進行修改衍腥,以適應新的變化磺樱;
- 類一旦設(shè)計完成,就可以獨立進行工作竹捉,不要再對其做任何修改。
不修改就意味著不影響現(xiàn)有業(yè)務块差,也就不會引發(fā)BUG。所以我們在設(shè)計類時倔丈,要考慮怎么樣既可以實現(xiàn)擴展功能憨闰,又不需要修改代碼。
這一點我體會很深刻需五,我現(xiàn)在公司的一個項目由于已經(jīng)上線了一年左右了,迭代了無數(shù)版本警儒,業(yè)務邏輯已經(jīng)比較復雜了眶根,大家現(xiàn)在寫代碼有點像是在修水管边琉,擰上一處閥門,往往導致另外一處地方漏水了族扰。。最后很多精力花在救火上面渔呵,導致生產(chǎn)效率越來越低砍鸠。
這里有幾個設(shè)計模式我推薦大家學習一下,可以讓代碼避免過早陷入復雜性:
- 裝飾者模式:Wrap一個新類來擴展功能
- 策略模式:制定一個策略接口爷辱,讓不同的策略實現(xiàn)成為可能
- 適配器模式:不改變原有類的基礎(chǔ)上適配新功能
- 觀察者模式:靈活添加和刪除觀察者(Listener)來擴展系統(tǒng)功能
里氏代換原則 LSP
程序中的父類都應該可以正確地被子類替換饭弓。我發(fā)現(xiàn)很多程序員寫代碼的時候,其實不太擅長使用繼承和抽象關(guān)系弟断。其實面向?qū)ο笤O(shè)計里面最偉大的概念就是抽象,理解了抽象昏翰,才能對現(xiàn)實世界的業(yè)務進行建模舍咖,才能化繁為簡锉桑,設(shè)計出簡潔的軟件架構(gòu)。
在進行抽象設(shè)計的時候民轴,程序中不應該出現(xiàn)instanceof關(guān)鍵詞,因為這種設(shè)計破壞了LSP原則瑰钮,將導致父類無法被復用(因為只有在特定子類時才生效)微驶。子類中使用的函數(shù)應該在父類中被定義浪谴,子類和父類在行為表現(xiàn)上一定要一致。
接口隔離原則 ISP
多個特定場景的接口篇恒,要好過一個寬泛的通用接口凶杖。
1、不要強迫用戶依賴那些他們并不使用的接口
2智蝠、使用多個專門的接口比使用一個總接口要好
這個比較好理解,就是我們現(xiàn)在都喜歡寫一個XXXService解虱,然后在里面定義一大堆的函數(shù)漆撞,這樣的寫法是違背ISP原則的,因為一個接口里面定義了太多的依賴函數(shù)叫挟,其實我們在其他地方調(diào)用該接口時,可能只依賴其中某幾個函數(shù)抹恳,但是卻要引入一個很大的依賴關(guān)系,如果修改了其中某個東西健霹,很容易導致其他地方出錯瓶蚂。所以盡量分成多個接口來開發(fā)。比如查詢窃这、修改分成2個接口來開發(fā)。
依賴倒轉(zhuǎn)原則 DIP
模塊之間交互應該依賴于抽象祟敛,而不是具體實現(xiàn)兆解。
1、大家依賴的是一個約定锅睛,而不關(guān)心具體實現(xiàn)細節(jié)
2历谍、領(lǐng)域?qū)硬粦撘蕾嚮A(chǔ)設(shè)施層
這2點非常重要辣垒,我們在寫代碼時,經(jīng)常會涉及到調(diào)用第三方模塊甜无,比如我訂單服務可能會查詢商品服務哥遮,那么我們之間應該首先建立好一個抽象接口約定,當我調(diào)用的時候眠饮,我只需要調(diào)用這個抽象接口就可以了,不需要關(guān)心其他服務是用什么框架寨蹋、甚至編程語言實現(xiàn)的扔茅。這里面Java里面的Spring框架是通過依賴注入方式來實現(xiàn),還有類似FeignClient這種也是一個很好的方式召娜。然后領(lǐng)域?qū)樱I(yè)務邏輯層)不應該依賴基礎(chǔ)設(shè)施層(框架、中間件等)秸讹,意思也是說雅倒,我們不應該讓自己的業(yè)務代碼被框架侵入太深,否則一旦這個框架有問題不滿足需要劣欢,那我們就比較被動了殖演。
總結(jié)
最近寫代碼比較多年鸳,遇到很多問題搔确,也總結(jié)了很多經(jīng)驗灭忠,以上只是一部分座硕。
PS:上周又遇到一個重大線上事故,我自己復盤下來华匾,收獲很大,回頭抽時間寫出來跟大家分享萨西,避免踩坑旭旭。