點擊關注公眾號,實用技術文章及時了解
寫了這麼多年的代碼,最近看了一本叫做《代碼整潔之道》的書籍,看完之後還是蠻有感悟的,特此記錄下從書中學習到的一些內容。
整潔的代碼來讓我們思考一個問題,什麼樣的代碼才算是好代碼?
關於這一點我大概整理了一些自己的看法,羅列了之後如下所示:
準確性
可以通過測試用例,滿足具體應用場景。
簡介性
沒有重複性的代碼,簡潔明了,沒有過多的繁瑣類的封裝,函數,方法。
看到這裡,你可能會想,說起來很能理解,但是實際工作中又該如何注意呢?別急,我們來看下邊這個反面案例:
@Override@Transactional(rollbackFor=Exception.class)publicPayResultDTOdoPay(PaymentOrderDTOpaymentOrderDTO){if(paymentOrderDTO.getAccountId()==null){returnPayResultDTO.payFail("參數不能為空");}CartDTOcartDTO=cartService.getMyCart(paymentOrderDTO.getAccountId());if(cartDTO==null){log.error("【dopay】購物車為空,支付異常accountIdis{}",paymentOrderDTO.getAccountId());returnPayResultDTO.payFail("購物車不存在,支付異常");}List<ProductDTO>productDTOList=CommonUtil.getProductListFromCart(cartDTO);for(ProductDTOproductDTO:productDTOList){booleanisEnough=productService.checkStock(productDTO);if(!isEnough){log.error("【dopay】庫存不足,productDTOis{}",productDTO);returnPayResultDTO.payStockNotEnough(productDTO);}}PaymentOrderDTOwaitPayOrderDTO=buildWaitPayOrderDTO(paymentOrderDTO.getAccountId(),productDTOList);PayResultDTOpayResultDTO=paymentOrderService.insertPaymentOrder(waitPayOrderDTO);log.info("【doPay】插入訂單信息,paymentOrderDTO={}",waitPayOrderDTO;if(!PayResultDTO.PAY_SUCCESS_CODE.equals(payResultDTO.getCode())){returnPayResultDTO.payFail(ORDER_CREATE_FAIL);}//處理微信簽名以及校驗AccountDTOaccountDTO=accountService.getAccountDTO(paymentOrderDTO.getAccountId());WXPayReqDTOwxPayReqDTO=WXPayReqDTO.builder().appId(MiniProgramConstants.APPID).openId(accountDTO.getOpenId()).mchId(MiniProgramConstants.MCH_ID)//單位是分.price((long)(waitPayOrderDTO.getActualAmount()*100)).orderNo(waitPayOrderDTO.getOrderNo()).attachParam(waitPayOrderDTO.getOrderNo()).body(MiniProgramConstants.DEFAULT_PAY_BODY).build();PayResultDTOwxPayResult=wxPayService.doPayReq(wxPayReqDTO);if(PayResultDTO.isPaySuccess(wxPayResult)){returnwxPayResult;}returnPayResultDTO.payFail(REQ_WX_ERROR);}這段代碼是早期我在工作時所寫的一段支付代碼,在進行支付之前,先判斷購物車裡的商品庫存是否充足,如果充足則進行扣減操作。但是現在回過頭來看這段代碼,你就會發現存在一些問題。
對於參數校驗,購物車校驗,庫存校驗,其實我們都可以加入一些類似於斷言組件之類的設計進行優化。
另外一些對象構建的代碼其實可以做一些封裝,因為調用這個接口的同事大多數時候都不會太願意去關心這些參數的構造過程,更多時候我們只需要將它封裝起來,然後通過一個函數的名字來告訴使用者就可以了。
進行優化之後的代碼可以變成如下所示:
@Override@Transactional(rollbackFor=Exception.class)publicPayResultDTOdoPay(PaymentOrderDTOpaymentOrderDTO){//使用斷言進行參數判斷BizAssertUtils.isNotNull(paymentOrderDTO.getAccountId(),ARG_ERROR);BizAssertUtils.isNotNull(cartService.getMyCart(paymentOrderDTO.getAccountId()),CART_NOT_NULL)List<ProductDTO>productDTOList=CommonUtil.getProductListFromCart(cartDTO);BizAssertUtils.isTrue(this.isStockEnough(productDTOList));//插入預先支付訂單,日誌記錄可以放在函數裡面PaymentOrderDTOwaitPayOrderDTO=buildWaitPayOrderDTO(paymentOrderDTO.getAccountId(),productDTOList);PayResultDTOpayResultDTO=paymentOrderService.insertPaymentOrder(waitPayOrderDTO);BizAssertUtils.isTrue(payResultDTO!=null&&!payResultDTO.isSuccess(),ORDER_CREATE_FAIL);//進行支付操作PayResultDTOwxPayResult=wxPayService.doPayReq(this.buildWXPayReqDTO(paymentOrderDTO.getAccountId(),waitPayOrderDTO));BizAssertUtils.isTrue(wxPayResult.isPaySuccess(),REQ_WX_ERROR));returnwxPayResult;}有意義的命名函數命名有的時候,我們對於函數的命名會有出現「名不副其實」的情況,例如下邊這個案例:
publicMap<String,Integer>getResultList(){//...}這段代碼中,返回的類型是Map類型,但是函數的命名卻以List結尾,這樣就給人有誤解的意思。如果函數的名字沒法給人很好的易讀性,就需要開發者再花費額外的時間趣深入理解,這就會顯得很費勁。再看下邊這個案例:
publiclong[]getList(intlen){long[]data=newlong[len];//隨機填充數據for(inti=0;i<len;i++){data[i]=newRandom().nextInt(5);if(data[i]<0){data[i]=0;}}returndata;}在這個函數裡面,會自動生成一個隨機數組,但是數字的值如果是負數,則會自動變為0。但是為什麼我們不在函數命名中進行定義呢?例如定義命名為:getNonnegativeArr(int len), 這樣不是更好理解函數的意思嘛。
變量命名對於函數變量的命名不要使用0或者o,1或者l很容易讓人看混,另外變量或者參數的命名也儘量使其具有意義,例如下邊這個案例所示:
publicbooleanisSubList(List<String>list1,List<String>list2){for(Stringitem:list1){if(!list2.contains(item)){returnfalse;}}returntrue;}例如這個案例中,參數命名採用了list1,list2,就很讓人容易誤解,不知道哪個參數是源集合,哪個參數是需要被比較的子集合。如果我們對它進行一些優化,看起來的效果就會明顯不同,例如:
publicbooleanisSubList(List<String>sourceList,List<String>compareList){for(Stringitem:sourceList){if(!compareList.contains(item)){returnfalse;}}returntrue;}使用好的變量命名可以讓人清晰了解到這個變量的類型是什麼樣的,另外在定義一些具有特殊含義的變量時候可以結合下具體的業務場景。例如下邊這段代碼,需要分別計算工作日 和 周末的訂單流水金額總數,
intj=7;for(inti=1;i<=j;i++){//周末的邏輯處理if(j-i<2){//...}else{//...}}其實在這裡我們可以賦予一個特殊的業務命名來表示,例如下邊這樣的寫法:
intTOTAL_DAYS_PER_WEEK=7;for(intdayOfWeek=1;dayOfWeek<=TOTAL_DAYS_PER_WEEK;dayOfWeek++){//周末的邏輯處理if(TOTAL_DAYS_PER_WEEK-dayOfWeek<2){//...}else{//...}}相對於上邊的變量i,j寫法,這種具有業務含義的變量命名更加能讓人明確它的具體含義。
函數的定義函數的命名
函數的命名儘量保證一個函數隻做一件事的原則,不要參雜過多的其他業務操作。
函數的參數個數
通常參數的個數建議最多設置在3個左右,如果參數過多容易讓人看了有誤解,此時可以嘗試用一些類去進行封裝。
函數的參數命名
對於有共同含義的參數,我們可以將它命名到同一個變量中,這樣能夠通過業務領域去定義他們,方便識別。
對於參數的命名可以結合一些工作術語,例如當我們需要定義一個隊列參數的時候可以定義JobQueue。
再來看下邊這個案例,我們定義了一個用戶基礎屬性類:
publicclassUserInfo{privatelongid;privateStringusername;privateStringtelNo;//用於表示地址信息privateStringname;//居住全地址privateStringnumber;//樓房號privateStringstreet;//街道名稱privateStringcity;//城市名稱}在這個案例中的name,number,street,city字段分別表示了用戶所居住的信息,但是如果我們不加以注釋或者沒有了解的同事和使用者預先說明的話,這些字段的具體含義是很難看出來的。這種時候我們可以嘗試給它加上一些特殊的業務前綴來表示,例如下邊兩種寫法:
classUserInfo{privateintid;privateStringusername;privateStringtelNo;//用於表示地址信息privateStringaddressName;//居住全地址privateStringaddressNumber;//樓房號privateStringaddressStreet;//街道名稱privateStringaddressCity;//城市名稱}不過更好的表示方式還是單獨設計一個對象來存儲這些具有明確業務含義的對象,例如定義一個Address對象來管理它們,例如下邊所示:
publicclassUserInfo{privateintid;privateStringusername;privateStringtelNo;//用於表示地址信息privateAddressaddress;}publicclassAddress{privateStringaddressName;//居住全地址privateStringaddressNumber;//樓房號privateStringaddressStreet;//街道名稱privateStringaddressCity;//城市名稱}注釋的規範不要太依賴於注釋關於代碼注釋方面,相信工作了一段時間的開發都會有以下想法:不太相信代碼注釋所寫的內容。為什麼會有這樣的想法呢?我感覺主要原因還是和代碼的不斷迭代有關,隨着代碼的不斷改動,其內部的流程早已經和注釋表達的含義背道而馳。所以有時候我們代碼注釋的意義並不是特別大。
有時候如果代碼注釋和核心內容不符的時候,它反而成了一段「謊言」的存在。所以我認為:唯一真正好的注釋是你要想辦法去不用寫注釋。
別寫廢話注釋在我早期工作的時候,這個點確實經常會犯,例如定義一個對象的時候,我幾乎會給每個字段都加入一行注釋,例如下邊這個類:
publicclassUserInfo{privateintid;//用戶名privateStringusername;//手機號privateStringtelNo;//性別privateIntegersex;//年齡privateIntegerage;//身份證號碼privateStringidCardNo;//郵件privateStringemail;}雖然說給每個字段都備註了注釋,看起來也似乎很規範,但是這樣的做法反而給人感覺寫了一堆廢話。通過每個字段的命名來看,難道它們還能有別的意思嘛?而且時間久了,別人看到這些無用的注釋之後也會自動過濾掉它們的含義。
代碼的長度通常我們定義的一個類,其包含的代碼量不建議設計得太大,大部分都濃縮在500行以內這個範圍就好了。這是因為通常短的內容會比長的內容更方便讓人理解,這就好比現如今的人更喜歡刷短視頻,但是對於一些長達幾十分鐘或者幾個小時的短片/電影卻沒有那麼感興趣。
除了好閱讀之外,在實際工作中,大家如果使用的電腦配置不高,在電腦運行了很久的情況下再去用編輯器去修改一些篇幅非常大的類,反而會塌癟慢。(我試過用Idea打開一份3000+行數的代碼文件,挪動一格光標大概會有1秒的延遲)
另外,也並不是說代碼的長度越短越好,有時候適當的空格與換行可以增加代碼片段的可讀性。
代碼的對齊在編寫代碼的時候,對於變量的對齊格式不同,其實也是有講究的。來看下邊兩組不同代碼的書寫風格:
變量的名稱統一對齊:
publicclassObj{privateSocketsocket;privateInputStreaminputStream;privateStringstr;privateFilefile;privateApplicationContextapplicationContext;privateAnnotationConfigApplicationContextannotationConfigApplicationContext;privateDubboApplicationContextInitializerdubboApplicationContextInitializer;}變量的類型統一對齊:
publicclassObj{privateSocketsocket;privateInputStreaminputStream;privateStringstr;privateFilefile;privateApplicationContextapplicationContext;privateAnnotationConfigApplicationContextannotationConfigApplicationContext;privateDubboApplicationContextInitializerdubboApplicationContextInitializer;}在工作中,我更加會傾向於使用第二種書寫格式,因為它給我的感覺會讓人更加清晰看到對應變量的格式類型是怎樣的。而對於第一種類型而言,給人感覺對於變量的類型不是那麼地關注。
你們有哪些很好的代碼習慣,歡迎分享。
推薦
Java面試題寶典
技術內卷群,一起來學習!!
PS:因為公眾號平台更改了推送規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。點「在看」支持我們吧!