一篇帶給你如何寫好一個Java類?
混沌之初
在進(jìn)行程序開發(fā)的過程中,我們有時會看到這樣的Java類:
- 有上百個公共方法
- 單個方法好幾百行
- 整個Java文件幾千行
先下結(jié)論,這樣的類顯然是不好的。盡管他勉強能維持當(dāng)前功能的運行。但實際上它已經(jīng)無法在進(jìn)行功能上的擴展了。我們對他能做的只有保守治療,在危樓上再添磚加瓦。
盡管大家都不愿意承認(rèn)自己是一片混沌的制造者,但實際上每一個巨型類的代碼都是由你我親手或間接締造的。
但是當(dāng)有一天我們意識到,這個類已經(jīng)太過巨大,需要進(jìn)行重構(gòu)的時候,我們需要一些方法論與準(zhǔn)則來幫助我們進(jìn)行判斷。
好的類是什么樣的
在實際的開發(fā)過程中,我們一眼就能判斷出來哪些類寫得好,哪些類寫的壞。我們可能不能明確地說出來個所以然,但是就是能感覺出來。這種原因是:
優(yōu)秀的類可能精準(zhǔn)地展現(xiàn)出它所具備的能力。
在我們進(jìn)行代碼編寫的時候,無時無刻的會和類打交道。而類與類之間所表達(dá)出來的邏輯之間的依賴和交互則是我們要關(guān)注的重點。所以,當(dāng)我們看到一個類時無法輕松的判斷出來它是用于什么功能的,或者一不小心的誤判了它實際的功能的時候,那么這個類就是有問題的。
以下我們針對哪些角度是可以幫助我們判斷一個類是否優(yōu)秀。
統(tǒng)一順序
我們在進(jìn)行代碼編寫的時候總是有各種各樣的規(guī)范,規(guī)范的目的并不是和實際的編碼人員對著干,主要目的在于減少團(tuán)隊中的溝通成本。所以對于編寫類文件來說,需要做的第一件事就是要統(tǒng)一所有類中內(nèi)容的排列順序。這樣做有兩個好處:
- 減少在編寫文件時候的位置考慮成本。
- 減少在閱讀代碼時的理解成本。
顯然我們更加關(guān)注的是第二點。具體來說我們要保證類的屬性在一起、類的方法在一起,以便在我們進(jìn)行代碼閱讀的時候不會錯過關(guān)鍵信息(我們更多的時候是簡單瀏覽一下類的全貌,然后就徑直的去找我們關(guān)注的內(nèi)容)。同時一般來說,我們按照以下順序來編寫類:
- 公共靜態(tài)常量
- 私有靜態(tài)常量
- 私有靜態(tài)變量(不太應(yīng)該有共有變量)
- 公有方法
- 公有方法所用到的私有方法
總的來說,我們應(yīng)該把屬性放到類的上面,而把方法放到類的下面,并將私有方法放到所調(diào)用的公有方法下面(可以參考《如何寫好一個方法》)。這樣便于在閱讀類中內(nèi)容的時候可以從上到下逐步地了解類中的細(xì)節(jié),符合我們自頂向下的閱讀習(xí)慣。
單一職責(zé)
我們有時候會覺得類可能太長了。有很多的原因會讓我們有這樣的想法,而其中比較重要的一個原因是:這個類同時承擔(dān)了復(fù)數(shù)個功能。
我們在進(jìn)行面向?qū)ο缶幊痰臅r候的主要方式是將擁有一些能力的對象用類的形式定義出來,這些能力就是類的方法。舉個例子:可以播放音樂的音箱,那么我們就可以創(chuàng)建一個音箱的類,然后其中有一個方法播放音樂。目前這個類是很好理解的,我們有一個音箱類,然后通過音箱類來實例化對象就可以得到一個音箱,通過調(diào)用音箱中的播放方法就可以播放音樂。但隨著功能的擴充,或許我們的音箱變成了移動音箱(注意,我們只有一個音箱),而且增加了一個充電功能,于是我們?yōu)檫@個類增加了一個充電方法。
隨著功能不斷地開發(fā),我們可能會為音箱類中增加許多的與其有關(guān)聯(lián)的事物,我們可能在音箱類中添加:充電、顯示時間、定時關(guān)閉、隨機播放等等工呢功能。在不斷地迭代之后這個類會變得非常的臃腫,但是判斷臃腫與否的條件,便是是否單一職責(zé)。
盡管單一職責(zé)的這個概念比較容易理解,但是在實際操作的時候卻沒有一個明確的邊界,也就是說要憑感覺。就比如上文中的音箱的例子,如果在只有充電和播放音樂的這種情況下,我們也可以將其寫入到一個類中。但是如果功能變多,比如增加了電量展示、涓流充電等功能,那顯然我們更應(yīng)該把這些方法放到一個電池的類中,并將電量的屬性也放進(jìn)去。
所以從可實施的角度上來說的話,我們可以通過兩種方法來幫我判斷這個方法是否滿足單一職責(zé):
- 能否為方法起一個合適的名字
- 能否通過句簡短的話來描述其功能。
解釋來說,如果無法用一個對象名來描述這個類的話,而只能通過一些通用概念(如處理器、執(zhí)行器、管理器)來對他進(jìn)行描述的話,就說明這個類的功能并不單一(當(dāng)然如果本身代碼規(guī)范就是這么定義的就另當(dāng)別論)。而如果無法用一兩句話來描述類的功能,或者必須用大量的“與”、“或”等詞來對描述做串聯(lián),就說明其承擔(dān)了太多的功能了,我們應(yīng)該將其拆分一下。
內(nèi)聚
我們在進(jìn)行面對對象編程的時候經(jīng)常說的就是類需要“高內(nèi)聚、低耦合”。我們把類中的方法操作類中的屬性稱做方法與屬性的關(guān)聯(lián)的話,那么關(guān)聯(lián)越多的類就是越內(nèi)聚的。極端一點的話,如果類內(nèi)所有的方法都使用所有的類屬性的話,那么這個類是最為內(nèi)聚的。實際情況并不總是如此理想,所以我們可以根據(jù)這種關(guān)聯(lián)關(guān)系來判斷類中的內(nèi)容是否足夠內(nèi)聚。
所以,如果你發(fā)現(xiàn)在在寫完類了之后部分方法只與部分屬性產(chǎn)生關(guān)聯(lián),而其他方法則與另外的屬性產(chǎn)生關(guān)聯(lián),就說明這兩部分之間是沒有內(nèi)聚性的。那么我們就可以將其拆分為兩個類,而這兩個類之間將更加的內(nèi)聚:方法與屬性互相依賴稱為了一個整體。
當(dāng)我們通過內(nèi)聚性來分析類后,可能會將一個大類,拆分為多個小類。這樣會增加類的數(shù)量從而增加類的復(fù)雜性,同時也會讓整體的代碼變多。但是類本身可以通過包路徑來進(jìn)行分類,所以這種拆分我認(rèn)為是比較合理的。
可擴展
對于一些有擴展需求的類,盡管他們可能滿足單一職責(zé)以及內(nèi)聚屬性。但是由于這種類本身的擴展性,導(dǎo)致我們會在新的業(yè)務(wù)需求的時候頻繁地對這種類進(jìn)行修改與新方法的增加。這種修改導(dǎo)致的問題是在每一次修改代碼或者新增方法的時候都無法保證不會對原有的功能造成影響。雖然我們可以通過單元測試或者集成測試來驗證我們的修改,但是這都會增加我們的工作量。
所以,對于可能會頻繁修改、并進(jìn)行業(yè)務(wù)追加的方法類,我們需要特別的為其保留擴展性。我們可以通過實現(xiàn)統(tǒng)一接口或者繼承抽象類、父類的方法,來獲得多個不同擴展能力的子類,而這些子類我們也可以通過策略模式或者責(zé)任鏈模式來組織。
對于類來說,對其進(jìn)行擴展總是好與對其直接進(jìn)行修改。
可測試
關(guān)于這個角度,其實是源于我的另一個問題,就是在傳統(tǒng)的三層框架下,中間的service層我們是否需要編寫一層接口(interface)。我原來的認(rèn)知是,實際上在絕大多數(shù)的情況,這個service我們是不會再進(jìn)行其他實現(xiàn)的,所以單獨寫一個接口然后再增加對應(yīng)的Impl只會讓我們在擴展方法的時候更加的繁瑣。
但是現(xiàn)在我發(fā)現(xiàn)了直接的用處,那就是:支持了單元測試的進(jìn)行。
如果我們直接依賴具體的細(xì)節(jié),就會對我們的測試帶來挑戰(zhàn)。具體來說,如果我們依賴于一個具體的類的實現(xiàn)的話,那么當(dāng)我們希望針對其中的細(xì)節(jié)進(jìn)行測試的話我們就要對細(xì)節(jié)之外的內(nèi)容進(jìn)行調(diào)整,可能包括:系統(tǒng)時間、數(shù)據(jù)庫字段等內(nèi)容。但如果我們是使用接口對servce進(jìn)行調(diào)用,那么在我們測試的時候就可以通過直接編寫測試用的service來實現(xiàn)預(yù)期的返回內(nèi)容。從而大大簡化進(jìn)行測試的難度。
我們通過接口降低了系統(tǒng)之間的耦合度,讓類之間的關(guān)系更好理解,也便于測試的進(jìn)行。而這也是我們的依賴倒置原則(DIP),我們針對抽象編程,讓其不依賴與具體細(xì)節(jié),這樣當(dāng)我們需要調(diào)整細(xì)節(jié)的時候也不會影響上層內(nèi)容。
最后
我們在coding的過程中總是在進(jìn)行類的編寫,本篇文章對類的一些編寫注意事項進(jìn)行了描述,在編寫過程中主要需要注意類的:內(nèi)部屬性方法順序、類的職責(zé)是否單一、是否足夠內(nèi)聚、是否支持?jǐn)U展、是否可以進(jìn)行單元測試。盡管這并非全部需要注意的內(nèi)容,但如果可以根據(jù)這些引發(fā)思考便足夠了。