找出那些代碼里的壞味道吧——《重構(gòu)》筆記(一)

image

寫(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)的條件邏輯

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容