代碼診所的第二次診斷
幾年前,我有機(jī)會(huì)負(fù)責(zé)一個(gè)項(xiàng)目的咨詢。團(tuán)隊(duì)很小,目標(biāo)是對(duì)舊有系統(tǒng)的后端用Java改寫,而團(tuán)隊(duì)的開發(fā)人員全為C程序員。我的工作職責(zé)是負(fù)責(zé)項(xiàng)目設(shè)計(jì)、開發(fā),以及擔(dān)任項(xiàng)目開發(fā)過程敏捷化的教練,并培養(yǎng)Java開發(fā)人員。
我在團(tuán)隊(duì)工作室的墻角落,開了一個(gè)小小的診所,廣而告之——“每日一貼,包治百病”。這是當(dāng)時(shí)我在項(xiàng)目上的第二次診斷。
1. 變量的聲明應(yīng)盡量與使用放在一起
本規(guī)則與代碼的可讀性有關(guān),倘若方法還沒有保持短小,這個(gè)問題就更要命?;蛟S這是C語言開發(fā)者容易犯的毛病。當(dāng)然也有許多Java程序員從前輩程序員處繼承了這一陋習(xí)。我曾經(jīng)在一個(gè)遺留項(xiàng)目中看到過一個(gè)長達(dá)幾千行的Java方法,在方法頭部堆砌了數(shù)十個(gè)變量定義,讓人目不暇給!
除了影響代碼的可讀性之外,還可能導(dǎo)致隱藏的缺陷。很多程序員之所以習(xí)慣在一開始就聲明變量,就是將這種局部變量當(dāng)做了存儲(chǔ)中間狀態(tài)的容器,在方法內(nèi)部反復(fù)使用該變量,這種中間結(jié)果的變遷未必符合開發(fā)者意圖,又或者后來的代碼維護(hù)者沒有理清這種變化,從而做出變量值的誤判。
2. 對(duì)常量和枚舉的使用
本規(guī)則本不足道,寫在這里,為了進(jìn)一步驚醒一下團(tuán)隊(duì)成員。在咨詢過程中,我看到有這段代碼:
- Integer.parseInt(freeFlash, 16);
 
這個(gè)16,究竟是什么鬼?Magic Number,很多時(shí)候會(huì)讓人感到困惑。
在JDK沒有提供枚舉之前,很多Java程序員喜歡使用接口類型來包裝一大堆常量。如果常量存在內(nèi)聚的分類意義,還是使用枚舉為佳。
3. 進(jìn)行合理封裝,避免方法調(diào)用順序錯(cuò)誤
封裝是非常有必要的。有時(shí)候,暴露太多的細(xì)節(jié)會(huì)讓調(diào)用者感到無可適從。
對(duì)于TelnetService類,我們需要依序調(diào)用connect()、login()、enterUShell(),然后在執(zhí)行命令后,必須依序執(zhí)行exitUShell(),disconnect()。這讓我想起事務(wù)處理,F(xiàn)TP訪問等與資源有關(guān)的邏輯,都需要在執(zhí)行邏輯前后包裹一些基礎(chǔ)設(shè)施的處理邏輯。為了避免在執(zhí)行命令前后忘記連接或斷開telnet,***能將此過程封裝。
這是從調(diào)用安全性來考慮。
如果從調(diào)用的簡潔性考慮,封裝亦有必要。當(dāng)我們需要通過TelnetService發(fā)送telnet命令時(shí),為何還需要了解內(nèi)部的執(zhí)行邏輯呢?
那么,該如何封裝才能兩全其美,既滿足對(duì)執(zhí)行邏輯順序的重用,又滿足對(duì)命令邏輯的擴(kuò)展?
通常做法是將真正的執(zhí)行邏輯提取為接口,如Java中Runnable的方式。這其實(shí)可以看作Command模式的運(yùn)用。當(dāng)然,我更愿意看做是對(duì)函數(shù)的封裝,例如Guva中的tranform()、filter()之類的方法,接受更具有函數(shù)氣質(zhì)的Function或者Predicate接口(當(dāng)時(shí),Java 8還未問世呢)。
因此,我的做法如下:
- public class TelnetService {
 - public T withCommand(ExecutionCommand<T> command) {
 - connect();
 - login();
 - enterUShell();
 - T result = command.send();
 - exitUShell();
 - disconnect();
 - return result;
 - }
 - }
 
可以這樣調(diào)用:
- String result = telnetService.withCommand(new ExecutionCommand<String>() {
 - @Override
 - public String send() {
 - return telnetService.transfer();
 - }
 - }
 - );
 
4. 遵循異常處理的架構(gòu)規(guī)則
團(tuán)隊(duì)成員對(duì)異常極為陌生,面對(duì)java的受控異常、非受控異常,不知如何選擇;也不知道該何時(shí)捕獲異常,何時(shí)拋出異常。因而我針對(duì)該項(xiàng)目確定了異常處理的架構(gòu)原則,其目的是為了讓整個(gè)架構(gòu)變得更簡單,讓異常處理更加一致。
我的目的是減輕開發(fā)人員的負(fù)擔(dān),但同時(shí)又不降低代碼質(zhì)量,并利于未來對(duì)代碼的維護(hù)。規(guī)則如下:
- 同層之間的調(diào)用不做try-catch,上層調(diào)用下層的對(duì)象,必須try-catch。即使對(duì)象拋出了異常,只要不是checked exception(我們盡量避免使用checked exception,以避免它對(duì)接口的污染),就無需考慮去捕獲這個(gè)異常。這樣的設(shè)計(jì)并不會(huì)導(dǎo)致異常泄露,因?yàn)槲覀円笤谏弦粚硬东@。至于最頂端的Application Layer,則只做捕獲異常的事兒,不干拋異常的活兒。
 - 為各層(即領(lǐng)域?qū)雍突A(chǔ)設(shè)施層)定義各自的異常超類。其中,領(lǐng)域?qū)佣x的異常要求提供Error Code。Error Code并非我所愿,但對(duì)于本系統(tǒng)的上游系統(tǒng),卻需要該值,不得不為。
 - 領(lǐng)域?qū)印H羰墙Y(jié)合實(shí)際情況由自己拋出異常,則只需考慮異常消息和錯(cuò)誤碼;若是捕獲了異常再拋出,則在捕獲時(shí)記錄日志,再度拋出的異常需要包裹原始異常對(duì)象。
 
在代碼診所中診斷出來的疾病,可以作為代碼評(píng)審的一個(gè)標(biāo)準(zhǔn),同時(shí)這些處方則可以當(dāng)做團(tuán)隊(duì)內(nèi)部分享與交流的知識(shí)庫。長期累積下來,非常有利于團(tuán)隊(duì)成員編碼能力的成長。
【本文為51CTO專欄作者“張逸”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】















 
 
 



 
 
 
 