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