在前面的文章中潭兽,我們介紹了 《提升編程效率:重構》 以及 《何時開始重構?》括授。了解了那些能夠更好的輔助團隊或者個人進行重構,但是要讓重構真正產(chǎn)生作用是需要能夠代碼中的壞味道岩饼,并消除代碼中的壞味道荚虚。
如下圖是工作中常見的代碼的壞味道:
上圖中的壞味道出自《重構》這本書,雖然并不是全部籍茧,但是涵蓋了日常中最常見的一些代碼壞味道版述。
接觸這些壞代碼可以分為三類:
見名知意的代碼壞味道:
稍微解釋即可掌握的代碼壞味道;
通過一些例子即可掌握的代碼的壞味道寞冯;
本文主要聚焦在“見名知意的代碼壞味道”渴析,后續(xù)兩類壞味道將在后續(xù)的文章中解釋說明:
1. 重復代碼
簡單的復制/粘貼,或者無意間添加了相同邏輯的代碼都是有可能的導致重復代碼出現(xiàn)的吮龄。
那么為什么重復的代碼是一種壞味道檬某?
最明顯的就是重復的代碼容易造成修改時的遺漏,修改遺漏導致一個問題需要修改多次才能才能確定最終修改完成螟蝙。如果有一部分修改了恢恼,另外一部分沒有修改且沒有被發(fā)現(xiàn),日后再遇到感覺類似胰默,實則不同的代碼會花費大量的時間確定業(yè)務上的需求场斑,實現(xiàn)上應該如何處理漓踢。
重復代碼這類壞味道產(chǎn)生的成本很低,但是帶來的影響卻是很大漏隐。
如何解決重復代碼問題喧半?
- Simple Design 為我們提供了參考參考原則:“通過測試,揭示意圖青责,消除重復挺据,最少元素”。
- 如果重復代碼發(fā)生在一個類中脖隶,且兩段代碼完全重復扁耐,可以借助 Extract Method (提煉函數(shù))這個重構手法來消除重復;提煉函數(shù)時 IDE 一般都會自動提示是否同時修改重復的代碼产阱,減少重構的工作量婉称。
- 如果重復代碼發(fā)生在一個類中,且兩段代碼之后部分重復构蹬。那么可以將部分重復的代碼通過 Extract Method 的手法王暗,提煉到單獨的方法中,并替換掉部分重復的代碼庄敛。
- 如果重復的代碼在不同的類中俗壹,且這些類是兄弟類,可以使用 Pull Up Method藻烤,將重復的代碼提煉到父類绷雏,并讓原本的類繼承父類。
- 如果重復的代碼在不同的類中隐绵,且這些類之間關聯(lián)性不大之众,那么可以 Extract Class拙毫,將重復的挪動到一個新的類中依许,原本出現(xiàn)重復的地方來調(diào)用這個新產(chǎn)生的類的方法。
- 消除重復之后缀蹄,檢測代碼表達的意圖是否準確峭跳、完成,Extract Method 時可以通過良好的方法名來解釋提煉的函數(shù)的作用和意圖缺前。
2 長函數(shù)
顧名思義蛀醉,長度過長的函數(shù)。其中包括兩種情況衅码,橫向過長拯刁,縱向過長。
為什么長函數(shù)是一種壞味道逝段?
橫向過長時垛玻,往往一眼無法快速了解該行代碼要表達的意思和中間的過程割捅。當出現(xiàn) Bug 定位問題時也不容易一次性定位到問題所在。
縱向過長時帚桩,往往會感覺某個函數(shù)內(nèi)部邏輯復雜亿驾、晦澀難懂。修改代碼中也會因為無法照顧到要修改的方法中的其他行代碼账嚎,而顧此失彼莫瞬,最終導致難度難修改。經(jīng)過多次修改后甚至原有的基本結構都會遭到破壞郭蕉,導致后續(xù)修改難度逐漸增加疼邀。
如何解決長函數(shù)的問題?
- 橫向過長的代碼恳不,可以通過代碼格式化檩小、CheckStyle插件來發(fā)現(xiàn)和消除。比如烟勋,Lambda 表達式规求,可以選擇在出現(xiàn)第一個“.”時就就開始換行。
List<Node> nodes = items.stream().filter(Item::isFree).filter(Item::notWork).map(Formater::format).filter(Node::hadChildren).filter(Node::hadMarked).collection(Collectors.toList());
這行代碼我們需要仔細讀 才能清楚中間的過程卵惦。采用首個“.”出現(xiàn)換行的將會是如下格式:
List<Node> nodes = items
.stream()
.filter(Item::isFree)
.filter(Item::notWork)
.map(Formater::format)
.filter(Node::hadChildren)
.filter(Node::hadMarked)
.collection(Collectors.toList());
通過對橫向代碼格式化能夠為代碼帶來更好的可讀性阻肿。當然你可以在提交代碼到倉庫時勾選上 commit 時自動格式化代碼的選項,避免沒有 Check Style 等工具來守護代碼沮尿,遺漏掉格式問題丛塌。
- 縱向過長的代碼。往往多個實現(xiàn)細節(jié)堆疊在一個方法中造成的畜疾,這種情況下使用 Inline Temp(內(nèi)聯(lián)局部變量)赴邻、 Extract Method 的重構手法來提煉小的函數(shù)。一個類中有很多零散的小函數(shù)也是常見的啡捶,因此提煉函數(shù)的同時記住姥敛,提煉函數(shù)的也是也是考慮創(chuàng)建新的類時候,將不同作用的函數(shù)提煉到響應職責的類中瞎暑。
- 縱向過長的代碼彤敛,往往存在職責不夠單一的情況,保持方法職責的單一有助于維護代碼的可讀性了赌。通過 2 中 提到的 Extract Method墨榄,那么某個具體實現(xiàn)細節(jié)可以被提煉到一個小函數(shù)中,而原來的函數(shù)則職責就編程調(diào)度作用勿她。所以方法的單一職責袄秩,更清晰的描述應該是一類事情,要么只在處理實現(xiàn)細節(jié),要么處理調(diào)度協(xié)調(diào)代碼調(diào)用之剧。
public class OrderService{
...
public Order create(OrderDTO orderDTO) {
// 創(chuàng)建條件是否符合 4 行
...
// 貨幣轉(zhuǎn)換 4 行
...
// 折扣計算 5 行
...
// 將 OrderDTO 轉(zhuǎn)換為 Order 對3行
...
// 存儲 Order 1 行
...
// 通知下有業(yè)務 5 行
...
return order;
}
}
看遺留系統(tǒng)時和面試作業(yè)的時候贮喧,總是看到這類代碼,可以通過提煉函數(shù)并遵守方法的單一職責原則猪狈,就能夠簡單的重構實現(xiàn)一個邏輯更為清晰的代碼結構箱沦,如下:
public class OrderService {
...
public Order create(OrderDTO orderDTO) {
verify(orderDTO);
Order order = orderRepository.save(orderMapper.toOrder());
notifyService.notify(order);
return order;
}
private void verify(OrderDTO orderDTO) {
// 創(chuàng)建條件是否符合 4 行
...
}
}
public interface OrderMapper {
...
public Order toOrder() {
// 將 OrderDTO 轉(zhuǎn)換為 Order 對3行
Currency currency = CurrentyTranslator.translator(currency); // 貨幣轉(zhuǎn)換
BigDecimal price = currentyTranslator.calculate(products); // 提煉函數(shù)
...
}
}
public class CurrentyTranslator {
public static Currency translate (Currency currency) {
// 貨幣轉(zhuǎn)換 4 行
...
}
}
public class PriceService {
public BigDecimal calculate(List<Product> products) {
// 折扣計算 5 行
...
return xxx;
}
}
public class NotifyService {
private void notify(Order order){
// 通知下有業(yè)務 5 行
...
}
}
上面只是一個簡單的重構方法,其中涉及到的重構手法:
? Move Field(搬移函數(shù))將上下文相關的變量挪動的一起雇庙;
? Extract Method (提煉函數(shù)) 將某個具體的實現(xiàn)提煉到一個職責單一的方法中谓形。
? Extract Method (提煉類)一個類尤其單獨的職責,因此將那些和原本的該類的職責關聯(lián)性不大的邏輯方法提煉到特定的類中疆前。
? Inline Field(內(nèi)聯(lián)臨時變量)如果一個變量對語意理解并沒有什么幫助寒跳,那么就可以采用內(nèi)聯(lián)臨時變量的方法,消除顯示的定義變量竹椒,從而減少代碼的行數(shù)童太,同時閱讀代碼時也會更加清爽、聚焦胸完。
更具實際業(yè)務場景還可以借助一些注解书释、工具類、AOP 來讓驗證赊窥、轉(zhuǎn)換爆惧、通知部分變得更加簡潔。通過提煉函數(shù)的重構手法锨能,能夠讓后續(xù)的重構更加方便可靠扯再。
如果翻閱一些開發(fā)規(guī)范會發(fā)現(xiàn)有的團隊規(guī)定一個方法不超過 15 行,其實知道這個規(guī)范只能獲取到一個參考量址遇,注意到行數(shù)多對熄阻,更重要的時候發(fā)現(xiàn)問題后的小步重構。
3 過大的類
顧名思義就是一個類做了太多的事情倔约。SOLID 原則告訴我們類的職責應該是單一的秃殉,而一個過大類很可能意味著承擔了多個/多類職責。
過大的類為什么是一種壞味道复濒?
由于過大的類承擔了過多的職責脖卖,很容易導致 重復代碼 且 重復代碼 不容易被發(fā)現(xiàn)乒省,而這往往是壞味道的開始。
如果過大的類對外提供服務發(fā)生了變動畦木,并不容易快速響應這樣的變化袖扛,可以對比一下一個小而職責單一的類中進行修改方便還是在多很多職責。
當過大的類因為某個地方發(fā)生變化,很可能導致不相關的調(diào)用方的代碼也會發(fā)生變化蛆封,這是一種耦合性的表現(xiàn)唇礁。
當過大的類被繼承時很可能導致其他的壞味道,例如遺留的饋贈惨篱。
因此盏筐,保持小而職責單一的類將會對系統(tǒng)的設計有很大的幫助。當然也可以參考 Simple Design砸讳,避免過度設計的前提下保持簡單的設計琢融。
如何解決過大的類的代碼壞味道?
- 觀察這個過大的類的屬性簿寂,看是否有關聯(lián)的幾個屬性能夠代表一定的業(yè)務意思漾抬,如果可以使用 Extract Class,將這幾個屬性挪動到一個新的類中常遂,并將相關操作挪動到新的類中纳令。循環(huán)往復,這樣一個大的類能夠拆分成多個小的且職責較為單一的類克胳。
- 觀察這個大類中的方法平绩,看是否存在兄弟關系的方法,如果有可以使用 Extract Subclass (提煉子類)的方法漠另,將相關方法提煉到子類中馒过,并考慮使用繼承父類還是面向接口使用 Extract Interface(提煉接口)。這樣相似行為的行為聚集在一個類中酗钞,拆分到多個類中腹忽,并可以進一步和方法的調(diào)用發(fā)來解耦。
- 進一步觀察剩余類的行為砚作,如果這些行為在處理一類事情窘奏,那么可以停止了,在處理多類事情葫录,可以按照處理邏輯的類型進一步拆分着裹。
簡而言之,使用一個亙古不變的法則:分治法米同。將過大的類骇扇,拆分成多個職責單一的小類,手段是 Extract Class面粮,Extract Subclass少孝,Extract Interface。
4 過長參數(shù)列表
當方法的參數(shù)列表過長時這也是一種代碼的壞味道熬苍。
?
為什么參數(shù)過長是一種壞味道稍走?
參數(shù)過長和過大的類袁翁、過長的函數(shù)、重復代碼一樣婿脸,起初并不會導致什么錯誤粱胜,但是代碼隨著時間向前演變過程,會給代碼帶來很多麻煩狐树。
長參數(shù)函數(shù)的可讀性很差焙压,尤其是存在多個類似長參數(shù)方法時,并不容易判斷出應該使用哪個方法抑钟。
當需要為長參數(shù)函數(shù)添加新的參數(shù)時冗恨,將會促使調(diào)用方發(fā)生變化,且新參數(shù)的位置也將讓這個方法更加難以理解味赃。
如何解決長參數(shù)的代碼壞味道掀抹?
- 如果傳遞的幾個參數(shù)都出自一個對象,那么可以選擇使用 Preserve Whole Object(保持完整對象)直接傳遞該對象心俗。
- 如果方法的參數(shù)來自不同的對象傲武,可以選擇使用 Introduce Parameter Object(引入?yún)?shù)對象)將多個參數(shù)放入一個新的類中,原來方法傳遞多個分開的參數(shù)城榛,現(xiàn)在傳遞一個包含多個屬性的一個對象揪利。
- 如果調(diào)用者先計算調(diào)用 A 方法得到計算結果,然后將計算結果在傳遞給這個長參數(shù)函數(shù)狠持,那么可以考慮去除這個參數(shù)疟位,改為在長參數(shù)函數(shù)中直接調(diào)用 A 得到結果,從而消除傳遞的部分參數(shù)喘垂,這個重構過程可以參考 Replace Parameter With Method(使函數(shù)替換參數(shù))??甜刻。
需要的注意的是,有些情況下長參數(shù)的存在也是合理的正勒,因為在一定程度上可以避免某些依賴關系的產(chǎn)生得院。可以通過觀察長參數(shù)函數(shù)變化的頻率章贞,并采用“事不過三祥绞,三則重構“的原則,保持進行重構的準備鸭限。
5 Switch 語句
Switch 語句代表一類語句蜕径,比如 if...else, switch... case 語句都是 switch 語句。
為什么 Switch 語句是一種代碼壞味道败京?
首先并不是所有的 Switch 語句都是壞味道兜喻,Swith 語句開發(fā)中常見的語句。這里帶有壞味道的 Switch 語句指的是那些造成重復代碼的 Switch語句喧枷。例如:根據(jù)某個狀態(tài)來判斷執(zhí)行執(zhí)行哪個動作虹统。
public Order nextStep(...) {
if (state == 1) {
// do something
} else if (state == 2) {
// do something
} else if (state == 3) {
// do something
} else {
// do something
}
}
這種實現(xiàn)方法很多代碼中都會出現(xiàn),但是多數(shù)人使用這種方式添加代碼隧甚,并不意味著這是一種好的代碼车荔。這樣的實現(xiàn)方式很容易造成長函數(shù),而且每次修改的位置要非常精準戚扳,需要在多個條件中逐個遍歷找到最終需要的那個忧便,再修改,可讀性上無疑也是很差的帽借。
如何處理 Switch 語句這種代碼壞味道呢珠增?**
- 如果 swtich 語句是某個方法的一部分,那么不妨使用 Extract Method(提煉函數(shù))將其先提煉出一個單獨的方法砍艾,縮小上下文范圍蒂教。
- 觀察多個條件中的動作的關聯(lián)關系,是否符合多態(tài)脆荷,如果是將符合多態(tài)的幾個條件創(chuàng)建對應的類凝垛,并使用 Move Method (移動函數(shù))移動到新創(chuàng)建的類中。
- 使用狀態(tài)模式蜓谋、枚舉等多種實現(xiàn)手段消除其中的 swtich 語句梦皮。
如果對有限狀態(tài)機感興趣可以參考文章:《Java有限狀態(tài)機的4種實現(xiàn)對比》
總而言之,一旦打算通過疊加新的 swtich case 來添加新邏輯桃焕,那么就應該關注一下代碼設計剑肯,因為這種操作很有可能就是為后續(xù)的代碼在挖坑。同時理解清楚那些swtich 語句是具有壞味道的語句观堂。
6 夸夸其談的未來性
這是工作中最常見的一類問題让网,比如如果你聽到這句話“我將文件上傳的實現(xiàn)做了調(diào)整 ... 未來再使用的時候?qū)?...”就應該警覺起來。
為什么夸夸其談的未來型是一種代碼壞味道师痕?
未來意味著當下并不是必須的寂祥,過度的抽象和提升復雜性也會讓系統(tǒng)難以理解和維護,同時也容易分散團隊的注意力七兜,如果用不到丸凭,那么就不值得做。
除非你在進行假設驅(qū)動開發(fā)腕铸,否則代碼上總是談未來容易綁架團隊的思想惜犀,拿未來不確定的事情來解釋事情的合理,會讓那些務實者狠裹,關注投入產(chǎn)出比的抉擇虽界。并且容易讓團隊進入一個假象。
當業(yè)務上變動時涛菠,并不能及時的將代碼進行變動撇吞,因為原來的代碼中包含了一種對未來假設的實現(xiàn),無形中增加了代碼的復雜度礁叔,而且很容易增加團隊溝通成本牍颈。
如何解決夸夸其談的未來性的代碼壞味道?
Simple Design (簡單設計原則)能夠幫助我們作出抉擇琅关。當實現(xiàn)業(yè)務代碼時考慮”通過測試煮岁、揭示意圖、消除重復涣易、最少元素“画机。
當發(fā)現(xiàn)為未來而寫的代碼時,可以:
- 刪除那些覺的未來有用的參數(shù)新症、代碼步氏、方法調(diào)用。
- 修正方法名徒爹,使方法名揭示當下業(yè)務場景的意圖戳护,避免抽象的技術描述詞。
通過上面兩個過程將代碼原本的要表達的意思還原回來瀑焦。
工作中有兩類未來性腌且。一類是假設調(diào)用方可以怎么使用;一類是未來必然發(fā)生的業(yè)務功能榛瓮。代碼的壞味道更多的指的是第一種情況铺董,第二種情況可以開發(fā)之前體現(xiàn)進行簡單設計和拆分,從而避免過度設計禀晓,同時可以避免談未來性精续,來讓代碼隨著功能一起小步重構并演進。
7 令人迷惑的臨時字段
在一些場景下為了在實現(xiàn)上的臨時方便性粹懒,有的開發(fā)者會直接在某個對象上添加一個屬性重付,后續(xù)使用在需要的時候使用該屬性。
令人迷惑的臨時字段的是什么代碼壞味道凫乖?
一個類包含屬性和方法确垫,屬性都是該類相關的。而臨時向類中添加的字段帽芽,雖然臨時有關聯(lián)性删掀,但是單獨來看這個類中的屬性時,卻會讓人覺得非常費解导街。有些接口的返回值就是也是類似原因?qū)е碌慕Y果披泪,每次為了方便像類中直接添加一些臨時屬性,滿足了當時的需要搬瑰,但是后續(xù)再使用的時候卻并不能區(qū)分哪些屬性時必須的款票,哪些是不必須的控硼,以及哪些被添加的字段的上下文分別是什么。
如何解決令人迷惑的臨時字段艾少?
- 問題的原因是隨意向類上添加字段卡乾,解決的方法就是將這個臨時字段移走,可以為這個字段找到一個合適的類來存放姆钉,也可以使用 Extract Class (提煉類)將這個字段添加到一個新類中说订,然后將該字段的相關的邏輯移動到該類中抄瓦,并確定該類的職責潮瓶。
- 可以將臨時字段作為參數(shù)進行傳遞,但是為了避免過長參數(shù)的出現(xiàn)钙姊,可以選擇將臨時字段提煉到一個新的類中毯辅。
8 過多的注釋
這是注釋降低代碼可讀性,甚至誤導了代碼要要表達的意圖煞额。
為什么過多的注釋是一種代碼壞味道思恐?
首先并不是所有的注視都是壞味道。
如果想通過注釋來表達代碼的意思膊毁,那么代碼修改了注釋也需要同步進行修改胀莹,如果代碼修改了但是沒有修正這是注釋就有可能導致誤導。
還有一種注釋的壞味道婚温,指的是不使用的代碼通過注釋掉來表示其棄用描焰。后續(xù)代碼的閱讀者會經(jīng)常收到斷斷續(xù)續(xù)的注釋掉的代碼影響。降低讀代碼和改代碼的速度栅螟。
在 《Clean Code》 中羅列了一些注釋的壞味道:
喃喃自語
多余的注釋
誤導性注釋
循規(guī)方注釋
日志式注釋
廢話注釋
用注釋來解釋變量意思
用來標記位置的注釋
類的歸屬的注釋
-
注釋掉的代碼
...
如何解決過多的注釋的代碼壞味道荆秦?
造成使用注釋的原因很多,可以考慮移除這些注釋:
- 刪除被注釋掉不再使用的代碼
- 如果某段代碼沒有辦法輕松的解釋清楚力图,可以使用 Extract Method 來步绸,并使用提煉的方法名來表達意圖。
- 刪除多余的注釋吃媒,誤導性注釋瓤介,如有必要可以將方法重命名,解釋意圖赘那。
- 用來說明變量意思的注釋刪除掉惑朦,對變量進行重命名,如果這個變量并不是必須的可以選擇將變量進行 Inline Temp漓概。
上面介紹了代碼中常見的 8 中代碼壞味道漾月,這些壞味道見名知意,每種壞味道通過簡單的幾步重構即可解決胃珍。面對這些壞味道應該避免延遲解決梁肿,隨時保持代碼的整潔蜓陌。