優(yōu)秀的后端應(yīng)該有哪些開發(fā)習(xí)慣?
前言
前后也待過幾家公司,碰到各種各樣的同事,見識過各種各樣的代碼,優(yōu)秀的、垃圾的、不堪入目的、看了想跑路的等等,所以這篇文章記錄一下一個優(yōu)秀的后端 Java 開發(fā)應(yīng)該有哪些好的開發(fā)習(xí)慣。
拆分合理的目錄結(jié)構(gòu)
受傳統(tǒng)的 MVC 模式影響,傳統(tǒng)做法大多是幾個固定的文件夾?controller、service、mapper、entity,然后無限制添加,到最后你就會發(fā)現(xiàn)一個?service?文件夾下面有幾十上百個 Service 類,根本沒法分清業(yè)務(wù)模塊。正確的做法是在寫?service?上層新建一個?modules?文件夾,在?moudles?文件夾下根據(jù)不同業(yè)務(wù)建立不同的包,在這些包下面寫具體的?service、controller、entity、enums?包或者繼續(xù)拆分。


等以后開發(fā)版本迭代,如果某個包可以繼續(xù)拆領(lǐng)域就繼續(xù)往下拆,可以很清楚的一覽項目業(yè)務(wù)模塊。后續(xù)拆微服務(wù)也簡單。
封裝方法形參
當你的方法形參過多時請封裝一個對象出來...... 下面是一個反面教材,誰特么教你這樣寫代碼的!
public void updateCustomerDeviceAndInstallInfo(long customerId, String channelKey,String androidId, String imei, String gaId,String gcmPushToken, String instanceId) {}
寫個對象出來
public class CustomerDeviceRequest {private Long customerId;//省略屬性......}
為什么要這么寫?比如你這方法是用來查詢的,萬一以后加個查詢條件是不是要修改方法?每次加每次都要改方法參數(shù)列表。封裝個對象,以后無論加多少查詢條件都只需要在對象里面加字段就行。而且關(guān)鍵是看起來代碼也很舒服啊!
封裝業(yè)務(wù)邏輯
如果你看過“屎山”你就會有深刻的感觸,這特么一個方法能寫幾千行代碼,還無任何規(guī)則可言......往往負責(zé)的人會說,這個業(yè)務(wù)太復(fù)雜,沒有辦法改善,實際上這都是懶的借口。不管業(yè)務(wù)再復(fù)雜,我們都能夠用合理的設(shè)計、封裝去提升代碼可讀性。下面貼兩段高級開發(fā)(假裝自己是高級開發(fā))寫的代碼
public ChildOrder submit(Long orderId, OrderSubmitRequest.Shop shop) {ChildOrder childOrder = this.generateOrder(shop);childOrder.setOrderId(orderId);//訂單來源 APP/微信小程序childOrder.setSource(userService.getOrderSource());// 校驗優(yōu)惠券orderAdjustmentService.validate(shop.getOrderAdjustments());// 訂單商品orderProductService.add(childOrder, shop);// 訂單附件orderAnnexService.add(childOrder.getId(), shop.getOrderAnnexes());// 處理訂單地址信息processAddress(childOrder, shop);// 最后插入訂單childOrderMapper.insert(childOrder);this.updateSkuInventory(shop, childOrder);// 發(fā)送訂單創(chuàng)建事件applicationEventPublisher.publishEvent(new ChildOrderCreatedEvent(this, shop, childOrder));return childOrder;}
@Transactionalpublic void clearBills(Long customerId) {// 獲取清算需要的賬單、deposit等信息ClearContext context = getClearContext(customerId);// 校驗金額合法checkAmount(context);// 判斷是否可用優(yōu)惠券,返回可抵扣金額CouponDeductibleResponse deductibleResponse = couponDeducted(context);// 清算所有賬單DepositClearResponse response = clearBills(context);// 更新 l_pay_depositlPayDepositService.clear(context.getDeposit(), response);// 發(fā)送還款對賬消息repaymentService.sendVerifyBillMessage(customerId, context.getDeposit(), EventName.DEPOSIT_SUCCEED_FLOW_REMINDER);// 更新賬戶余額accountService.clear(context, response);// 處理清算的優(yōu)惠券,被用掉或者解綁couponService.clear(deductibleResponse);// 保存券抵扣記錄clearCouponDeductService.add(context, deductibleResponse);}
這段兩代碼里面其實業(yè)務(wù)很復(fù)雜,內(nèi)部估計保守干了五萬件事情,但是不同水平的人寫出來就完全不同,不得不贊一下這個注釋,這個業(yè)務(wù)的拆分和方法的封裝。一個大業(yè)務(wù)里面有多個小業(yè)務(wù),不同的業(yè)務(wù)調(diào)用不同的 service 方法即可,后續(xù)接手的人即使沒有流程圖等相關(guān)文檔也能快速理解這里的業(yè)務(wù),而很多初級開發(fā)寫出來的業(yè)務(wù)方法就是上一行代碼是 A 業(yè)務(wù)的,下一行代碼是 B業(yè)務(wù)的,在下面一行代碼又是 A 業(yè)務(wù)的,業(yè)務(wù)調(diào)用之間還嵌套這一堆單元邏輯,顯得非常混亂,代碼還多。
判斷集合類型不為空的正確方式
很多人喜歡寫這樣的代碼去判斷集合
if (list == null || list.size() == 0) {return null;}
當然你硬要這么寫也沒什么問題......但是不覺得難受么,現(xiàn)在框架中隨便一個 jar 包都有集合工具類,比如?org.springframework.util.CollectionUtils、com.baomidou.mybatisplus.core.toolkit.CollectionUtils?。以后請這么寫
if (CollectionUtils.isEmpty(list) || CollectionUtils.isNotEmpty(list)) {return null;}
集合類型返回值不要 return null
當你的業(yè)務(wù)方法返回值是集合類型時,請不要返回 null,正確的操作是返回一個空集合。你看 mybatis 的列表查詢,如果沒查詢到元素返回的就是一個空集合,而不是 null。否則調(diào)用方得去做 NULL 判斷,多數(shù)場景下對于對象也是如此。
映射數(shù)據(jù)庫的屬性盡量不要用基本類型
我們都知道 int/long 等基本數(shù)據(jù)類型作為成員變量默認值是 0。現(xiàn)在流行使用 mybatisplus 、mybatis 等 ORM 框架,在進行插入或者更新的時候很容易會帶著默認值插入更新到數(shù)據(jù)庫。我特么真想砍了之前的開發(fā),重構(gòu)的項目里面實體類里面全都是基本數(shù)據(jù)類型。當場裂開......
封裝判斷條件
public void method(LoanAppEntity loanAppEntity, long operatorId) {if (LoanAppEntity.LoanAppStatus.OVERDUE != loanAppEntity.getStatus()&& LoanAppEntity.LoanAppStatus.CURRENT != loanAppEntity.getStatus()&& LoanAppEntity.LoanAppStatus.GRACE_PERIOD != loanAppEntity.getStatus()) {//...return;}
這段代碼的可讀性很差,這 if 里面誰知道干啥的?我們用面向?qū)ο蟮乃枷肴ソo?loanApp?這個對象里面封裝個方法不就行了么?
public void method(LoanAppEntity loan, long operatorId) {if (!loan.finished()) {//...return;}
LoanApp 這個類中封裝一個方法,簡單來說就是這個邏輯判斷細節(jié)不該出現(xiàn)在業(yè)務(wù)方法中。
/*** 貸款單是否完成*/public boolean finished() {return LoanAppEntity.LoanAppStatus.OVERDUE != this.getStatus()&& LoanAppEntity.LoanAppStatus.CURRENT != this.getStatus()&& LoanAppEntity.LoanAppStatus.GRACE_PERIOD != this.getStatus();}
控制方法復(fù)雜度
推薦一款 IDEA 插件?CodeMetrics?,它能顯示出方法的復(fù)雜度,它是對方法中的表達式進行計算,布爾表達式,if/else 分支,循環(huán)等。

點擊可以查看哪些代碼增加了方法的復(fù)雜度,可以適當進行參考,畢竟我們通常寫的是業(yè)務(wù)代碼,在保證正常工作的前提下最重要的是要讓別人能夠快速看懂。當你的方法復(fù)雜度超過 10 就要考慮是否可以優(yōu)化了。
使用 @ConfigurationProperties 代替 @Value
之前居然還看到有文章推薦使用 @Value 比 @ConfigurationProperties 好用的,吐了,別誤人子弟。列舉一下 @ConfigurationProperties 的好處。
在項目?
application.yml?配置文件中按住 ctrl + 鼠標左鍵點擊配置屬性可以快速導(dǎo)航到配置類。寫配置時也能自動補全、聯(lián)想到注釋。需要額外引入一個依賴?org.springframework.boot:spring-boot-configuration-processor?。

@ConfigurationProperties 支持 NACOS 配置自動刷新,使用 @Value 需要在 BEAN 上面使用 @RefreshScope 注解才能實現(xiàn)自動刷新
@ConfigurationProperties 可以結(jié)合 Validation 校驗,@NotNull、@Length 等注解,如果配置校驗沒通過程序?qū)硬黄饋恚霸绲陌l(fā)現(xiàn)生產(chǎn)丟失配置等問題。
@ConfigurationProperties 可以注入多個屬性,@Value 只能一個一個寫
@ConfigurationProperties 可以支持復(fù)雜類型,無論嵌套多少層,都可以正確映射成對象
相比之下我不明白為什么那么多人不愿意接受新的東西,裂開......你可以看下所有的 springboot-starter 里面用的都是 @ConfigurationProperties 來接配置屬性。
推薦使用 lombok
當然這是一個有爭議的問題,我的習(xí)慣是使用它省去?getter、setter、toString?等等。
不要在 AService 調(diào)用 BMapper
我們一定要遵循從?AService -> BService -> BMapper,如果每個 Service 都能直接調(diào)用其他的 Mapper,那特么還要其他 Service 干嘛?老項目還有從 controller 調(diào)用 mapper 的,把控制器當 service 來處理了。。。
盡量少寫工具類
為什么說要少寫工具類,因為你寫的大部分工具類,在你無形中引入的 jar 包里面就有,String 的,Assert 斷言的,IO 上傳文件,拷貝流的,Bigdecimal 的等等。自己寫容易錯還要加載多余的類。
不要包裹 OpenFeign 接口返回值
搞不懂為什么那么多人喜歡把接口的返回值用 Response 包裝起來......加個?code、message、success?字段,然后每次調(diào)用方就變成這樣
CouponCommonResult bindResult = couponApi.useCoupon(request.getCustomerId(), order.getLoanId(), coupon.getCode());if (Objects.isNull(bindResult) || !bindResult.getResult()) {throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);}
這樣就相當于
在 coupon-api 拋出異常
在 coupon-api 攔截異常,修改 Response.code
在調(diào)用方判斷 response.code 如果是 FAIELD 再把異常拋出去......
你直接在服務(wù)提供方拋異常不就行了么。。。而且這樣一包裝 HTTP 請求永遠都是 200,沒法做重試和監(jiān)控。當然這個問題涉及到接口響應(yīng)體該如何設(shè)計,目前網(wǎng)上大多是三種流派
接口響應(yīng)狀態(tài)一律 200
接口響應(yīng)狀態(tài)遵從HTTP真實狀態(tài)
佛系開發(fā),領(lǐng)導(dǎo)怎么說就怎么做
不接受反駁,我推薦使用 HTTP 標準狀態(tài)。特定場景包括參數(shù)校驗失敗等一律使用 400 給前端彈 toast。下篇文章會闡述一律 200 的壞處。
寫有意義的方法注釋
這種注釋你寫出來是怕后面接手的人瞎么......
/*** 請求電話驗證** @param credentialNum* @param callback* @param param* @return PhoneVerifyResult*/
要么就別寫,要么就在后面加上描述......寫這樣的注釋被 IDEA 報一堆警告看著蛋疼
和前端交互的 DTO 對象命名
什么 VO、BO、DTO、PO 我倒真是覺得沒有那么大必要分那么詳細,至少我們在和前端交互的時候類名要起的合適,不要直接用映射數(shù)據(jù)庫的類返回給前端,這會返回很多不必要的信息,如果有敏感信息還要特殊處理。
推薦的做法是接受前端請求的類定義為?XxxRequest,響應(yīng)的定義為?XxxResponse。以訂單為例:接受保存更新訂單信息的實體類可以定義為?OrderRequest,訂單查詢響應(yīng)定義為?OrderResponse,訂單的查詢條件請求定義為?OrderQueryRequest。
盡量別讓 IDEA 報警
我是很反感看到 IDEA 代碼窗口一串警告的,非常難受。因為有警告就代表代碼還可以優(yōu)化,或者說存在問題。前幾天捕捉了一個團隊內(nèi)部的小bug,其實本來和我沒有關(guān)系,但是同事都在一頭霧水的看外面的業(yè)務(wù)判斷為什么走的分支不對,我一眼就掃到了問題。

因為 java 中整數(shù)字面量都是?int?類型,到集合中就變成了?Integer,然后?stepId?點上去一看是?long?類型,在集合中就是?Long,那這個?contains?妥妥的返回?false,都不是一個類型。
你看如果注重到警告,鼠標移過去看一眼提示就清楚了,少了一個生產(chǎn) bug。
盡可能使用新技術(shù)組件
我覺得這是一個程序員應(yīng)該具備的素養(yǎng)......反正我是喜歡用新的技術(shù)組件,因為新的技術(shù)組件出現(xiàn)必定是解決舊技術(shù)組件的不足,而且作為一個技術(shù)人員我們應(yīng)該要與時俱進~~ 當然前提是要做好準備工作,不能無腦升級。舉個最簡單的例子,Java 17 都出來了,新項目現(xiàn)在還有人用 Date 來處理日期時間...... 都什么年代了你還在用 Date
結(jié)語
本篇文章簡單介紹我日常開發(fā)的習(xí)慣,當然僅是作者自己的見解。暫時只想到這幾點,以后發(fā)現(xiàn)其他的會更新。
