
寫(xiě)在前面
重構(gòu)起源于smalltalk,發(fā)揚(yáng)于java和C#,它們都有成熟的重構(gòu)工具。有一種說(shuō)法是,《重構(gòu)》和設(shè)計(jì)模式是java行業(yè)的圣經(jīng)。我個(gè)人覺(jué)得,重構(gòu)就像修繕忒休斯之船一樣,只是我們是將船上的木板全部替換成了鋼板。一個(gè)程序員如果看自己一年前寫(xiě)的代碼而沒(méi)有重構(gòu)的念頭,那么這個(gè)程序員可能這一年沒(méi)有什么進(jìn)步,當(dāng)然也可能這塊代碼已經(jīng)不需要重構(gòu)了,但我想這種概率挺低的。
因?yàn)楦鞣N原因,沒(méi)有人能在框架設(shè)計(jì)一開(kāi)始就能套用設(shè)計(jì)模式寫(xiě)好,所以我們的代碼需要不斷的重構(gòu)。Gof的設(shè)計(jì)模式就是重構(gòu)的目標(biāo)。但是重構(gòu)也是必須有理論準(zhǔn)備的,必須系統(tǒng)化的進(jìn)行,否則可能引入不可察覺(jué)的錯(cuò)誤,風(fēng)險(xiǎn)更大。
本文記錄的重點(diǎn)只在于指出代碼里的壞味道,如果在你的代碼中“聞到”了這些壞味道就說(shuō)明這塊的代碼可能需要重構(gòu)了,至于怎么重構(gòu),書(shū)中有一套系統(tǒng)的方法,請(qǐng)期待后續(xù)的文章。
最后,希望重構(gòu)能變成像空氣和水一樣普通的技術(shù)。
代碼的壞味道有如廚房的油污,開(kāi)始時(shí)不會(huì)覺(jué)得有多大的影響,但時(shí)間長(zhǎng)了就會(huì)累積成“惡心”又難以“清除”的污漬。我們需要保持每天的清掃,而不是定期的“大掃除”。上面的“味道”就是一點(diǎn)一點(diǎn)的“油星”濺在“廚房”里,看到它們就順手擦掉吧!
一、重構(gòu)原則
1.重構(gòu)的定義
重構(gòu)(名詞):對(duì)軟件內(nèi)部結(jié)構(gòu)的一種調(diào)整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。
重構(gòu)(動(dòng)詞):使用一系列重構(gòu)手法,在不改變軟件可觀察行為的前提下,調(diào)整其結(jié)構(gòu)。
-
關(guān)于重構(gòu)需要強(qiáng)調(diào)的兩點(diǎn):
- 重構(gòu)的目的是使軟件更容易被理解和修改。
- 重構(gòu)不會(huì)改變軟件可觀察的行為——重構(gòu)之后軟件功能一如以往。
2.重構(gòu)的目標(biāo)
- 進(jìn)行重構(gòu)之后,我們希望我們的程序能達(dá)到以下幾點(diǎn)目標(biāo):
- 容易閱讀
- 所有邏輯都只在唯一地點(diǎn)指定
- 新的改動(dòng)不會(huì)危及現(xiàn)有行為
- 盡可能簡(jiǎn)單表達(dá)條件邏輯
3.何時(shí)不該重構(gòu)
- 現(xiàn)有代碼根本不能正常運(yùn)行時(shí),應(yīng)該重寫(xiě),而不應(yīng)重構(gòu)。
- 如果項(xiàng)目已近最后期限,你也應(yīng)該避免重構(gòu)。
二、代碼的壞味道
-
Duplicate Code(重復(fù)代碼)
- 如果你在一個(gè)以上的地點(diǎn)看到相同的程序結(jié)構(gòu),那么可以肯定,設(shè)法將它們合二為一,程序會(huì)變得更好。
-
Long Method(過(guò)長(zhǎng)函數(shù))
- 擁有短函數(shù)的對(duì)象會(huì)活的比較好,比較長(zhǎng)。
- 我們遵循這樣一條原則:每當(dāng)感覺(jué)需要以注釋來(lái)說(shuō)明點(diǎn)什么的時(shí)候,我們就把需要說(shuō)明的東西寫(xiě)進(jìn)一個(gè)獨(dú)立函數(shù)中,并以其用途(而非實(shí)現(xiàn)手法)命名。
- 條件表達(dá)式和循環(huán)常常也是提煉的信號(hào)。
-
Large Class(過(guò)大的類(lèi))
- 如果想利用單個(gè)類(lèi)做太多事情,其內(nèi)往往就會(huì)出現(xiàn)太多實(shí)例變量。
- 和“太多實(shí)例變量”一樣,類(lèi)內(nèi)如果有太多代碼,也是代碼重復(fù)、混亂并最終走向死亡的源頭。最簡(jiǎn)單的解決方案是把多余的東西消弭于類(lèi)內(nèi)部。
- 這里有個(gè)技巧:先確定客戶(hù)端如何使用它們(指“太多的實(shí)例變量”),然后運(yùn)用Extract Interface為每一種使用方式提煉出一個(gè)接口。這或許可以幫助你看清楚如何分解這個(gè)類(lèi)。
-
Long Parameter List(過(guò)長(zhǎng)參數(shù)列)
- 如果你手上沒(méi)有所需的東西,總可以叫另一個(gè)對(duì)象給你。因此,有了對(duì)象,你就不必把函數(shù)需要的所有東西都以參數(shù)傳遞給它了,只需傳給它足夠的、讓函數(shù)能從中獲得自己需要的東西就行了。
-
Divergent Change(發(fā)散式變化)
- 指的是,如果一個(gè)類(lèi)中引入一個(gè)新的變化,需要修改多個(gè)函數(shù),則需要考慮將這個(gè)類(lèi)一分為二。
- 針對(duì)某一外界變化的所有相應(yīng)修改,都只應(yīng)該發(fā)生在單一類(lèi)中,而這個(gè)新類(lèi)內(nèi)的所有內(nèi)容都應(yīng)該反應(yīng)此變化。
-
Shotgun Surgery(霰彈式修改)
- Divergent Change是指“一個(gè)類(lèi)受多種變化的影響”,Shotgun Surgery則是指“一種變化引發(fā)多個(gè)類(lèi)相應(yīng)的修改”。這兩種情況下你都會(huì)希望整理代碼,使“外界變化”與“需要修改的類(lèi)”趨于一一對(duì)應(yīng)。
-
Feature Envy(依戀情結(jié))
- 函數(shù)對(duì)某個(gè)類(lèi)的興趣高過(guò)對(duì)自己所處類(lèi)的興趣。
- 判斷哪個(gè)類(lèi)擁有最多被此函數(shù)使用的數(shù)據(jù),然后就把這個(gè)函數(shù)和那些數(shù)據(jù)擺在一起。
- 最根本的原則是:將總是一起變化的東西放在一塊兒。數(shù)據(jù)和引用這些數(shù)據(jù)的行為總是一起變化的,但也有例外。如果例外出現(xiàn),我們就搬移那些行為,保持變化值在一地發(fā)生。
-
Data Clumps(數(shù)據(jù)泥團(tuán))
- 你常??梢栽诤芏嗟胤娇吹较嗤娜捻?xiàng)數(shù)據(jù):兩個(gè)類(lèi)中相同的字段、許多函數(shù)簽名中相同的參數(shù)。這些總是綁在一起出現(xiàn)的數(shù)據(jù)真應(yīng)該擁有屬于它們自己的對(duì)象。
- 一個(gè)好的評(píng)判辦法是:刪除眾多數(shù)據(jù)中的一項(xiàng)。這么做,其他數(shù)據(jù)有沒(méi)有因而失去意義?如果它們不再有意義,這就是個(gè)明確信號(hào):你應(yīng)該為它們產(chǎn)生一個(gè)新對(duì)象。
-
Primitive Obsession(基本類(lèi)型偏執(zhí))
- 對(duì)象的一個(gè)極大的價(jià)值在于:它們模糊(甚至打破)了橫亙于基本類(lèi)型和體積較大的類(lèi)之間的界限。你可以輕松編寫(xiě)一些與語(yǔ)言?xún)?nèi)置(基本)類(lèi)型無(wú)異的小型類(lèi)。
- 對(duì)象技術(shù)的新手通常不愿意在小任務(wù)上運(yùn)用小對(duì)象——像是結(jié)合數(shù)值和幣種的money類(lèi)、由一個(gè)起始值和一個(gè)結(jié)束值組成的range類(lèi)、電話(huà)號(hào)碼或郵政編碼等的特殊字符串。你可以運(yùn)用Replace Data Value With Object將原來(lái)單獨(dú)存在的數(shù)據(jù)值替換為對(duì)象,從而走出傳統(tǒng)的洞窟,進(jìn)入炙手可熱的對(duì)象世界。
-
Switch Statements(switch驚悚現(xiàn)身)
- 面向?qū)ο蟪绦虻囊粋€(gè)最明顯特征就是:少用switch(或case)語(yǔ)句。
- 大多數(shù)時(shí)候,一看到switch語(yǔ)句,你就應(yīng)該考慮以多態(tài)來(lái)替換它。
- 如果你只是在單一函數(shù)中有些選擇事例,且并不想改動(dòng)它們,那么多態(tài)就有點(diǎn)殺雞用牛刀了。
-
Parallel Inheritance Hierarchies(平行繼承體系)
- 每當(dāng)你為某個(gè)類(lèi)增加一個(gè)子類(lèi),必須也為另一個(gè)類(lèi)相應(yīng)增加一個(gè)子類(lèi)。
- 消除這種重復(fù)性的一般策略是:讓一個(gè)繼承體系的實(shí)例引用另一個(gè)繼承體系的實(shí)例。
-
Lazy Class(冗贅類(lèi))
- 你所創(chuàng)建的每一個(gè)類(lèi),都得有人去理解它,維護(hù)它,這些工作都是要花錢(qián)的,如果一個(gè)類(lèi)的所得不值其身價(jià),它就應(yīng)該消失。
-
Speculative Generality(夸夸其談未來(lái)性)
- 當(dāng)有人說(shuō)“嗷,我想我們總有一天需要做這事”,并因而企圖以各種各樣的鉤子和特殊情況來(lái)處理一些非必要的事情,這種壞味道就出現(xiàn)了。
- 如果所有裝置都會(huì)被用到,那就值得那么做;如果用不到,就不值得。用不上的裝置只會(huì)擋你的路,所以,把它搬開(kāi)吧。
-
Temporary Field(令人迷惑的暫時(shí)字段)
- 有時(shí)你會(huì)看到這的對(duì)象:其內(nèi)某個(gè)實(shí)例變量?jī)H為某個(gè)特定情況而設(shè)。
- 請(qǐng)使用Extract Class給這個(gè)可憐的孤兒創(chuàng)造一個(gè)家,然后把所有和這個(gè)變量相關(guān)的代碼都放進(jìn)這個(gè)新家。
-
Message Chains(過(guò)度耦合的消息鏈)
- 如果你看到用戶(hù)向一個(gè)對(duì)象請(qǐng)求另一個(gè)對(duì)象,然后再向后者請(qǐng)求另一個(gè)對(duì)象,然后再請(qǐng)求另一個(gè)對(duì)象……這就是消息鏈。
- 先觀察消息鏈最終得到的對(duì)象是用來(lái)干什么的,看看能否以Extract Method把使用該對(duì)象的代碼提煉到一個(gè)獨(dú)立函數(shù)中,再運(yùn)用Move Method把這個(gè)函數(shù)推入消息鏈。如果這條鏈上的某個(gè)對(duì)象有多位客戶(hù)打算航行此航線(xiàn)的剩余部分,就加一個(gè)函數(shù)來(lái)做這件事。
-
Middle Man(中間人)
- 人們可能過(guò)度運(yùn)用委托。你也許會(huì)看到某個(gè)類(lèi)接口有一半的函數(shù)都委托給其他類(lèi),這樣就是過(guò)度運(yùn)用。這時(shí)應(yīng)該使用Remove Middle Man,直接和真正負(fù)責(zé)的對(duì)象打交道。
-
Inappropriate Intimacy(狎昵關(guān)系)
- 有時(shí)你會(huì)看到兩個(gè)類(lèi)過(guò)于親密,花費(fèi)太多時(shí)間去探究彼此的private成分。
- 就像古代戀人一樣,過(guò)分狎昵的類(lèi)必須拆散。你可以采用Move Method和Move Field幫它們劃清界線(xiàn),從而減少狎昵行徑。
- 繼承往往造成過(guò)度親密,因?yàn)樽宇?lèi)對(duì)超類(lèi)的了解總是超過(guò)后者的主觀愿望。
-
Alternative Classes With Different Interfaces(異曲同工的類(lèi))
- 如果兩個(gè)函數(shù)做同一件事,卻有著不同的簽名。請(qǐng)運(yùn)用Rename Method根據(jù)它們的用途重新命名。但這往往不夠,請(qǐng)反復(fù)運(yùn)用Move Method將某些行為移入類(lèi),直到兩者的協(xié)議一致為止。
-
Incomplete Library Class(不完美的類(lèi)庫(kù))
- 麻煩的是庫(kù)往往構(gòu)造得不夠好,而且往往不可能讓我們修改其中的類(lèi)使它完成我們希望完成的工作。
- 如果你只想修改類(lèi)庫(kù)的一兩個(gè)函數(shù),可以運(yùn)用Introduce Foreign Method;如果想要添加一大堆額外行為,就得運(yùn)用Introduce Local Extension。
-
Data Class(純稚的數(shù)據(jù)類(lèi))
- 所謂Data Class是指:它們擁有一些字段,以及用于訪(fǎng)問(wèn)(讀寫(xiě))這些字段的函數(shù),除此之外一無(wú)長(zhǎng)物。
- Data Class就行小孩子,作為一個(gè)起點(diǎn)很好,但若要讓它們像成熟的對(duì)象那樣參與整個(gè)系統(tǒng)的工作,它們就必須承擔(dān)一定責(zé)任。
-
Refused Bequest(被拒絕的遺贈(zèng))
- 如果子類(lèi)復(fù)用了超類(lèi)的行為(實(shí)現(xiàn)),卻又不愿意支持超類(lèi)的接口,Refused Bequest的壞味道就會(huì)變得濃烈。拒絕繼承超類(lèi)的實(shí)現(xiàn),這一點(diǎn)我們不介意;但如果拒絕繼承超類(lèi)的接口,我們不以為然。不過(guò)即使你不愿意繼承接口,也不要胡亂修改繼承體系,應(yīng)該運(yùn)用Replace Inheritance With Delegation來(lái)達(dá)到目的。
-
Comments(過(guò)多的注釋?zhuān)?/strong>
- 常常會(huì)有這樣的情況:你看到一段代碼有著長(zhǎng)長(zhǎng)的注釋?zhuān)缓蟀l(fā)現(xiàn),這些注釋之所以存在乃是因?yàn)榇a很糟糕。
- 如果你不知道該做什么,這才是注釋的良好運(yùn)用時(shí)機(jī)。
三、兩句重要的話(huà)
- 重構(gòu)的基本技巧——小步前進(jìn),頻繁測(cè)試。
- 模式是你希望到達(dá)的目標(biāo),重構(gòu)則是到達(dá)之路。
本系列文章
找出那些代碼里的壞味道吧——《重構(gòu)》筆記(一)
重構(gòu),第一個(gè)案例(C++版)——最初的程序
重構(gòu),第一個(gè)案例(C++版)——分解并重組Statement()
重構(gòu),第一個(gè)案例(C++版)——運(yùn)用多態(tài)取代與價(jià)格相關(guān)的條件邏輯