title: 輔助審查系統(tǒng)的代碼書寫規(guī)范
date: 2019/10/26 09:43
remark: 本系統(tǒng)采用SpringBoot + Dubbo進(jìn)行開發(fā)
前言
近期在重構(gòu)輔助審查系統(tǒng)囚灼,發(fā)現(xiàn)隨著項目的發(fā)展涯雅,代碼變得異常混亂,完全違背了當(dāng)初定下來的規(guī)范,當(dāng)然這其實是無法避免的,畢竟時間緊任務(wù)重,哪里有時間讓你深思熟慮的考慮這段代碼該怎樣寫仙逻。
之前的代碼規(guī)范只在自己的心中,隨著時間就會慢慢遺忘涧尿,在寫新的代碼的時候系奉,可能就想不到那么多,從而導(dǎo)致代碼的“味道”越來越差姑廉,由此我決定花一天時間缺亮,將其落實到紙上,日后寫代碼的時候可以看一下桥言,盡量保證代碼的味道不要太差萌踱。
當(dāng)然,由于項目時間緊任務(wù)重号阿,可能沒辦法所有的代碼都按照規(guī)范來寫并鸵,有的時候只能尋找一些捷徑,這樣也是不可避免的倦西;希望大家日后有時間的時候能真,可以按照本規(guī)范,將違背了規(guī)范的地方進(jìn)行重構(gòu)扰柠。
注:由于輔助審查系統(tǒng)的特殊性,本規(guī)范可能不適用于其它系統(tǒng)疼约。
一卤档、工程架構(gòu)模型
1.1 如何分層
本系統(tǒng)采用的分層結(jié)構(gòu)與p3c規(guī)范中的基本相同。
Dao層
數(shù)據(jù)訪問對象(Data Access Object)程剥,用于對數(shù)據(jù)庫進(jìn)行訪問劝枣,負(fù)責(zé)數(shù)據(jù)的CURD。
當(dāng)然 Dao 不僅限于與數(shù)據(jù)庫進(jìn)行交互织鲸,假如日后系統(tǒng)引入的ElasticSearch舔腾、Mongo 甚至Redis,我認(rèn)為都可以定義一個Dao對其進(jìn)行訪問搂擦。
這樣的定義稳诚,可以將數(shù)據(jù)的CURD和業(yè)務(wù)邏輯進(jìn)行分離。
Manager層
p3c規(guī)范中對其定義如下:
- 對第三方平臺封裝的層瀑踢,預(yù)處理返回結(jié)果及轉(zhuǎn)化異常信息
- 對 Service 層通用能力的下沉扳还,如緩存方案才避、中間件通用處理
- 與 DAO 層交互,對多個 DAO 的組合復(fù)用
由這個定義并結(jié)合我們的系統(tǒng)氨距,可以得出Manager層主要作用為:
- 可以通過 Manager 層調(diào)用第三方服務(wù)(指標(biāo)系統(tǒng)桑逝、全文檢索服務(wù),因為他們是基礎(chǔ)服務(wù)俏让,所以不要在 web 層調(diào)用他們)楞遏,對返回結(jié)果進(jìn)行簡單的處理之后返回 Service 層。
- 如果需要對基礎(chǔ)服務(wù)進(jìn)行調(diào)用首昔,并將其結(jié)果處理后入庫(例如:檔案管理系統(tǒng)橱健、模型系統(tǒng))這種用法時,直接通過 Dao 層進(jìn)行保存沙廉。
- 可以將 Manager 理解為對通用邏輯的封裝拘荡,避免 Service 與 Service 進(jìn)行相互調(diào)用,以及對通用邏輯的管理撬陵。
在開發(fā)中珊皿,我們經(jīng)常會遇到 AService 中的某個業(yè)務(wù)可以提供給 BService 調(diào)用,從而讓 BService 調(diào)用 AService 的方法巨税,認(rèn)為是 Service 之間具有共同的業(yè)務(wù)蟋定。其實 Service 之間沒有共同的業(yè)務(wù),而是具備通用的邏輯草添,這時應(yīng)該將其抽離出來放在 Manager 中驶兜。無論何種工程架構(gòu)都好,我都不贊同 Service 與 Service 之間的相互調(diào)用远寸。
- 如果2個(或2個以上)表之間有一定的關(guān)聯(lián)關(guān)系(一對多抄淑、多對多)并經(jīng)常一起使用,則通過一個 Manager 對他們進(jìn)行訪問驰后。
- 可以在這層加入緩存(當(dāng)然我們體量小肆资,一般只在service層加緩存),與我上面所說的為操作Redis定義對Dao進(jìn)行訪問灶芝。
Service層
對具體對業(yè)務(wù)邏輯進(jìn)行操作(復(fù)用性很低)郑原,由于我們使用的是dubbo,Service層可能會被其他人調(diào)用夜涕,所以最好還是做一下參數(shù)校驗(hibernate validate)
從p3c規(guī)范來看犯犁,如果不用Manager層來對多個Dao進(jìn)行組合,那么Service層可以直接與Dao進(jìn)行交互女器,但是這樣會給人一種很混亂對感覺酸役,所以我們定義:
Service對數(shù)據(jù)庫進(jìn)行操作時,必須通過Manager層,其實這也是為日后開發(fā)可能遇到對問題留有余地簇捍;如果我們使用Redis做Service層緩存的話只壳,那么可以直接調(diào)用對應(yīng)Redis的Dao。
Web層
web層只做簡單對參數(shù)校驗暑塑,或者簡單對業(yè)務(wù)處理等(例如:查詢審查任務(wù)實體吼句,但是前端只需要部分字段,進(jìn)行轉(zhuǎn)換的工作)事格;Web層與Service層一一對應(yīng)惕艳。
1.2 分層領(lǐng)域?qū)ο?/h3>
本系統(tǒng)采用對模型為貧血領(lǐng)域模型,p3c規(guī)范定義對數(shù)據(jù)傳輸對象過多驹愚,這樣就導(dǎo)致了一個對象可能會出現(xiàn)3次甚至4次轉(zhuǎn)換在一次請求中远搪,當(dāng)返回的時候同樣也會出現(xiàn)3-4次轉(zhuǎn)換,這樣有可能一次完整的“請求-返回”會出現(xiàn)很多次對象轉(zhuǎn)換逢捺。如果在開發(fā)中真的按照這么來谁鳍,恐怕就別寫其他的了,一天就光寫這個重復(fù)無用的邏輯算了吧劫瞳,所以我們只定義了幾個對象倘潜。
貧血領(lǐng)域模型中對象只作為數(shù)據(jù)載體,只有 getter/setter 方法志于,而不包含業(yè)務(wù)方法涮因。
DO(Data Object)
數(shù)據(jù)對象,對數(shù)據(jù)源數(shù)據(jù)的映射伺绽,如數(shù)據(jù)庫表养泡,ElasticSearch 索引的數(shù)據(jù)結(jié)構(gòu)。所在包一般命名為 data 奈应。
DTO(Data Transfer Object)
數(shù)據(jù)傳輸對象澜掩,業(yè)務(wù)層向外傳輸?shù)膶ο蟆H绻?strong>某個業(yè)務(wù)中需要多次查詢獲取不同的數(shù)據(jù)對象钥组,最后將會把這多個數(shù)據(jù)對象組合成一個 DTO 并對外傳輸输硝。所在包命名為 dto 。
VO(View Object)
顯示層對象程梦,通常是 Web 向模板渲染引擎層傳輸?shù)膶ο蟆,F(xiàn)在的項目多數(shù)為前后端分離橘荠,后端只需要返回 JSON 屿附,那么可以理解為 JSON 即是需要渲染成的“模板”。我一般會將這類對象命名為 xxxResponse 哥童,所在包命名為 response挺份。
Query
數(shù)據(jù)查詢對象,數(shù)據(jù)查詢對象贮懈,各層接收上層的查詢請求匀泊。
其實一般用于 Controller 接受傳過來的參數(shù)优训,可以將其都命名為 xxxQuery,而我個人習(xí)慣將放在 request body 的參數(shù)(即 @RequestBody)包裝為 xxxRequest 各聘,而如果使用表單傳輸過來的參數(shù)(即 @RequestParam)包裝為 xxxForm 揣非,并分別放在包 request 和包 form 下。
注:web層向service層傳輸對query對象躲因,絕對不能傳輸?shù)組anager層早敬,因為Manager層是通用的邏輯。
層間對象傳遞
其中DTO如果不可復(fù)用大脉,那么可以直接傳輸給前端搞监。
1.3 包結(jié)構(gòu)及其含義
輔助審查服務(wù)模塊包設(shè)計
x5456deMBP:dgp-dubbo-server-root x5456$ tree dgp-ars-server-service/src -d -L 8
dgp-ars-server-service/src
├── main
│ ├── java
│ │ └── com
│ │ └── dist
│ │ └── ars
│ │ ├── aop
│ │ ├── config
│ │ ├── dao
│ │ ├── manager
│ │ │ └── remote
│ │ │ ├── ams
│ │ │ ├── ims
│ │ │ ├── mms
│ │ │ ├── pms
│ │ │ └── sms
│ │ └── service
│ └── resources
│ ├── META-INF
│ │ ├── dubbo
│ │ └── services
│ ├── config
│ ├── db
│ │ ├── oracle
│ │ │ ├── create
│ │ │ └── update
│ │ └── pg
│ │ └── create
│ └── libs
└── test
└── java
└── com
└── dist
└── ars
├── service
└── manager
輔助審查Api模塊包設(shè)計
tree dgp-ars-server-api/src -d -L 8
dgp-ars-server-api/src
└── main
└── java
└── com
└── dist
└── ars
├── constants
├── exceptions
├── helper # web層與service共用的輔助類
├── model
│ ├── dto
│ ├── entity
│ ├── query
│ │ ├── request
│ │ ├── form
│ │ ├── webQuery # 由web層封裝向service層傳輸?shù)牟樵儗ο? │ │ └── commonQuery # 由service層封裝傳輸?shù)組anager層的通用查詢對象
│ └── vo
└── service
helper包
開發(fā)中會遇到一些很基礎(chǔ)的,通用的業(yè)務(wù)邏輯镰矿,例如我們可能會根據(jù)每個用戶的信息生成一個唯一的 account id 琐驴。又或者說有一個用戶排名的需求,我們將從用戶的相關(guān)信息中計算出一個分?jǐn)?shù)秤标,從而根據(jù)這個分?jǐn)?shù)進(jìn)行排名绝淡。那么這時候我們可能會將這些邏輯寫在 User 數(shù)據(jù)對象或是其他相應(yīng)對應(yīng)的數(shù)據(jù)對象下。
由于我們采用的是貧血領(lǐng)域模型抛杨,數(shù)據(jù)對象中不應(yīng)該包含業(yè)務(wù)邏輯够委,所以我們將這些通用的業(yè)務(wù)邏輯都抽出來,放到 helper 包中進(jìn)行統(tǒng)一管理怖现。如會將生成 account id 的邏輯放在 AccountIdGenerator 中茁帽,將計算排名分?jǐn)?shù)的邏輯放在 RankCalculator 中。
我將這些類都?xì)w為 Helper 屈嗤,用于提供底層的業(yè)務(wù)計算邏輯潘拨。而為什么不放在通用工具層中呢?因為這些 Helper 其實都是依賴于特定的領(lǐng)域饶号,即特定的業(yè)務(wù)铁追。而通用工具類則是業(yè)務(wù)無關(guān)的,任何系統(tǒng)茫船,只要有需要都可以引用琅束。
二、代碼風(fēng)格
2.1 命名規(guī)范
https://mp.weixin.qq.com/s/WLHXrdfKc71b0EU0vi09gA
類名使用名詞或者形容詞 + 名詞算谈。
方法名為動詞或動詞短語涩禀。
包名使用小寫,只能有一個自然語意的英語單詞然眼。包名使用單數(shù)艾船,但如果類名有復(fù)數(shù)含義,則可以使用復(fù)數(shù)。
抽象類以Base/Abstract開頭屿岂;異常類以Exception結(jié)尾践宴;測試類以被測類名開頭,Test結(jié)尾爷怀;枚舉采用Enum結(jié)尾阻肩。
2.2 Google Java編程規(guī)范
源文件結(jié)構(gòu)
1、許可證或版權(quán)信息(如有需要)
2霉撵、package語句
3磺浙、import語句
4、一個頂級類(只有一個)
注:以上每個部分之間用一個空行隔開
類成員順序
1徒坡、變量
2撕氧、構(gòu)造方法
3、公有方法
4喇完、getter/setter方法
5伦泥、私有方法
注:重載方法永不分離。
換行
一般情況下锦溪,一行長代碼超出列限制(80或100個字符)不脯,我們就需要將其分為多行。
換行的基本準(zhǔn)則是:更傾向于在更高的語法級別處斷開刻诊。
- 如果在非賦值運(yùn)算符處斷開防楷,那么在該符號前斷開(比如+,它將位于下一行)则涯。
- 如果在賦值運(yùn)算符處斷開复局,通常的做法是在該符號后斷開(比如=,它與前面的內(nèi)容留在同一行)粟判。這條規(guī)則也適用于 foreach 語句中的分號亿昏。
- 方法名或構(gòu)造函數(shù)名與左括號留在同一行。
- 逗號(,)與其前面的內(nèi)容留在同一行档礁。
換行時角钩,至少縮進(jìn)4個空格。
空行
以下情況需要使用一個空行:
- 類內(nèi)連續(xù)的成員之間:字段呻澜,構(gòu)造函數(shù)递礼,方法,嵌套類羹幸,靜態(tài)初始化塊宰衙,實例初始化塊。
- 在函數(shù)體內(nèi)睹欲,語句的邏輯分組間使用空行。
- 類內(nèi)的第一個成員前或最后一個成員后的空行是可選的(既不鼓勵也不反對這樣做,視個人喜好而定)窘疮。
變量聲明
不要組合聲明袋哼,例如:
int a,b = 0;
變量需要使用時才聲明
2.3 p3c規(guī)范總結(jié)
http://www.reibang.com/p/329dd85cde4f
2.4 Effective Java總結(jié)
http://www.reibang.com/p/61e8b5b96e98
三、單元測試
單元測試是針對程序的最小單元來進(jìn)行正確性檢驗的測試工作闸衫。程序單元是應(yīng)用的最小可測試部件涛贯。一個單元可能是單個程序、類蔚出、對象弟翘、方法等。
3.1 為什么要寫單元測試
提高代碼質(zhì)量
對一個單元進(jìn)行測試時骄酗,需要將其隔離外部的依賴(數(shù)據(jù)庫稀余、第3方接口),保證外部依賴不影響當(dāng)前單元的邏輯趋翻。
正因為如此睛琳,他會促進(jìn)我們對工程進(jìn)行組件化拆分,整理工程依賴關(guān)系踏烙,更大程度減少代碼耦合师骗。
提升重構(gòu)自信心
重構(gòu),每個開發(fā)者都會經(jīng)歷讨惩,重構(gòu)后把代碼改壞了的情況并不少見辟癌。以往,寫完一個框架荐捻,運(yùn)行一下黍少,沒什么問題,完事靴患;由于最初的框架并不是你寫的仍侥,可謂牽一發(fā)動全身,你改1個方法導(dǎo)致整個框架運(yùn)行失敗鸳君。有了單元測試后农渊,我們重構(gòu)時自然就會多一分勇氣。
測試驅(qū)動開發(fā)(TDD):
測試驅(qū)動開發(fā)是戴兩頂帽子思考的開發(fā)方式:先戴上實現(xiàn)功能的帽子或颊,在測試的輔助下砸紊,快速實現(xiàn)其功能;再戴上重構(gòu)的帽子囱挑,在測試的保護(hù)下醉顽,通過去除冗余的代碼,提高代碼質(zhì)量平挑。測試驅(qū)動著整個開發(fā)過程:首先游添,驅(qū)動代碼的設(shè)計和功能的實現(xiàn)系草;其后,驅(qū)動代碼的再設(shè)計和重構(gòu)唆涝。
3.2 單元測試的原則
AIR原則
A:Automatic(自動化)
I:Independent(獨(dú)立性找都,不同的單元測試之間要互相獨(dú)立)
R:Repeatable(可重復(fù)執(zhí)行)
BCDE原則
編寫單元測試時要保證測試粒度足夠小,這樣有助于精確定位問題廊酣,用例默認(rèn)是方法級別的能耻。單測不負(fù)責(zé)檢查跨類或者跨系統(tǒng)的交互邏輯,那是集成測試需要覆蓋的范圍亡驰。編寫單元測試用例時晓猛,為了保證被測模塊的交付質(zhì)量,需要符合BCDE原則凡辱。
- B: Border戒职,邊界值測試,包括循環(huán)邊界煞茫、特殊取值帕涌、特殊時間點(diǎn)、數(shù)據(jù)順序等续徽。
- C: Correct蚓曼,正確的輸入,并得到預(yù)期的結(jié)果钦扭。
- D: Design纫版,與設(shè)計文檔相結(jié)合,來編寫單元測試客情。(沒有設(shè)計文檔其弊,不懂這條什么意思)
- E: Error,單元測試的目標(biāo)是證明程序有錯膀斋,而不是程序無錯梭伐。為了發(fā)現(xiàn)代碼中潛在錯誤,我們需要編寫測試用例時仰担,有一些強(qiáng)制的錯誤輸入(如非法數(shù)據(jù)糊识、異常流程、非業(yè)務(wù)允許輸入等)來得到預(yù)期的結(jié)果摔蓝。
使用Mock對象
由于單元測試只是系統(tǒng)集成測試前的小模塊測試赂苗,有些因素往往是不具備的,因此需要進(jìn)行Mock贮尉,例如:
(1)功能因素拌滋。比如被測試方法內(nèi)部調(diào)用的功能不可用。
(2)時間因素猜谚。比如雙十一還沒有到來败砂,與此時間相關(guān)的功能點(diǎn)赌渣。
(3)環(huán)境因素。政策環(huán)境吠卷,如支付寶政策類新功能;多端環(huán)境锡垄,如PC、手機(jī)等祭隔。
(4)數(shù)據(jù)因素。線下數(shù)據(jù)樣本過小路操,難以覆蓋各種線上真實場景疾渴。
(5)其他因素。為了簡化測試編寫屯仗,開發(fā)者也可以將一些復(fù)雜的依賴采用Mock方式實現(xiàn)搞坝。
優(yōu)秀的單元測試
(1)單元測試是“白盒測試”碎乃,應(yīng)該覆蓋各個分支流程赂弓、異常條件蹬竖。
(2)單元測試面向的是一個單元(Unit)秧饮,是由Java中的一個類或者幾個類組成的單元摧茴。
(3)單元測試的運(yùn)行速度一定要快!
(4)單元測試一定是可重復(fù)執(zhí)行的!
(5)單元測試之間不能有相互依賴赘理,應(yīng)該是獨(dú)立的!
(6)單元測試代碼和業(yè)務(wù)代碼同等重要翘紊,要一并維護(hù)!
3.3 怎樣寫
結(jié)合到本系統(tǒng)换薄,普通的增刪改查這樣過于簡單的功能就不需要進(jìn)行測試了鞠呈;
我們主要是對Manager層和Service層這兩層進(jìn)行測試融师,因為這兩層主要設(shè)計到了數(shù)據(jù)的處理。
Service層的單元測試引用Manager層的Mock對象蚁吝,Manager層引用Dao層的Mock對象旱爆。
如果像mms那樣具有復(fù)雜的邏輯,我們就要將其進(jìn)一步拆分成很小的單元進(jìn)行測試窘茁。
Demo
@ActiveProfiles("prod")
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ArsServiceApplication.class)
public class ProjectReviewInfoDmnImplTest {
@Autowired
private IProjectReviewInfoManager projectReviewInfoDmn;
@MockBean
private VProjectReviewInfoRepository vProjectReviewInfoRepository;
// 湖州市區(qū)域code
private String regionCode = "330500000000";
@Test
public void findProjectReviewInfo() {
this.mockFindProjectReviewInfo();
// 湖州市及其下面層級的區(qū)域code
List<String> subRegionCodeList = JsonUtils.toList("[\"330500000000\",\"330501000000\",\"330502000000\",\"330503000000\",\"330521000000\",\"330522000000\",\"330523000000\"]", String.class);
CommonReviewTaskQuery commonReviewTaskQuery = new CommonReviewTaskQuery();
commonReviewTaskQuery.setAreaLevel(StatusEnum.AreaLevelEnum.CITY.desc());
commonReviewTaskQuery.setPlanType(StatusEnum.PlanTypeEnum.LAND_SPACE_PLAN.desc());
commonReviewTaskQuery.setRolesName(Collections.singletonList("市總規(guī)科"));
commonReviewTaskQuery.setRegionCodeList(subRegionCodeList);
commonReviewTaskQuery.setKeyword("湖州");
commonReviewTaskQuery.setTaskAreaLevel(StatusEnum.AreaLevelEnum.CITY.code());
commonReviewTaskQuery.setQueryApprovalStage(false);
List<VProjectReviewInfo> result = projectReviewInfoDmn.findProjectReviewInfo(commonReviewTaskQuery);
Assert.assertEquals(JsonUtils.toString(result), "xxx");
}
@SuppressWarnings("unchecked")
private void mockFindProjectReviewInfo() {
String result = "xxx";
Mockito.when(vProjectReviewInfoRepository.findAll(ArgumentMatchers.any(Specification.class), ArgumentMatchers.any(Sort.class)))
.thenReturn(JsonUtils.toList(result, VProjectReviewInfo.class));
}
}
3.4 總結(jié)
單元測試確實會帶給你相當(dāng)多的好處怀伦,但不是立刻體驗出來。正如買重疾保險山林,交了很多保費(fèi)房待,沒病沒痛,十幾年甚至幾十年都用不上捌朴,最好就是一輩子用不上理賠吴攒,身體健康最重要。單元測試也一樣砂蔽,寫了可以買個放心洼怔,對代碼的一種保障,有bug盡快測出來左驾,沒bug就最好镣隶,總不能說“寫那么多單元測試极谊,結(jié)果測不出bug,浪費(fèi)時間”吧安岂?
以下是個人對單元測試一些建議:
- 越重要的代碼轻猖,越要寫單元測試;
- 代碼做不到單元測試域那,多思考如何改進(jìn)咙边,而不是放棄;
- 邊寫業(yè)務(wù)代碼次员,邊寫單元測試败许,而不是完成整個新功能后再寫;
- 多思考如何改進(jìn)淑蔚、簡化測試代碼市殷。
四、重構(gòu)
http://www.reibang.com/p/e5276d50a7b5
五刹衫、日志
本文參考文章
第一部分
1醋寝、應(yīng)用分層模型
第二部分
3婶肩、Google Java 編程規(guī)范(中文版)
第三部分
1办陷、談?wù)劄槭裁磳憜卧獪y試
3律歼、Mockito與PowerMock的使用基礎(chǔ)教程
定期對公司項目進(jìn)行基礎(chǔ)代碼的重構(gòu)民镜。合理的拆分業(yè)務(wù)無關(guān)的基礎(chǔ)代碼。
最好不要直接引用三方庫险毁,進(jìn)行再次的封裝
更新代碼時同時更新注釋和單元測試
盡量少寫代碼(lombok)
pom文件的管理(待google)
防御式編程:不要相信任何外來參數(shù)
類制圈、變量命名
日志、狀態(tài)碼