程序員必備寶典:手把手教你清理垃圾代碼
0. 值得清理么?
第一件你需要問問自己的事情就是代碼值得清理么。我不是說當(dāng)問到是否要清理代碼時,你一定要回答是或者一定回答不是。是你對代碼負(fù)有責(zé)任,也是你需要一直面對它們直到最終寫出的代碼是你樂意維護(hù)的,也是你很自豪的放入代碼庫的。
如果你覺得就算代碼看起來很可怕,也不值得浪費你本來就很緊張的時間來修復(fù)它們。所以你僅僅做了最最微小的調(diào)整解救燃眉之急。
換句話說,你也可以將代碼看作自己的,也可以看作是別人的。
兩種情況都有優(yōu)缺點。優(yōu)秀的程序員看到爛代碼時會覺得很難受。他們會拿出火把和叉子并且高呼:“太亂了,太亂了”。這是一種優(yōu)秀的品質(zhì)。
但是清理代碼是一個繁雜的工作。很容易就低估了時間。甚至有時候和從頭開始寫代碼一樣的耗時。并且短期并沒有帶來任何的短期效應(yīng)。兩個星期的時間清理代碼并不會帶來任何新的功能,但有可能引入一些新的錯誤。
另一方面,如果長時間不清理代碼可能會帶來災(zāi)難性的毀滅。混亂是代碼的殺手。
所以,這并不是一個容易做出的決定。需要考慮一些事情:
● 你期望對這段代碼做多少改變?你是希望僅僅修改這個小錯誤呢,還是這段代碼還要使用多次,所以你希望將它“調(diào)教”的好些,并且加上新的功能。如果僅僅是修復(fù)一個錯誤,那么最好是別打草驚蛇。然而,如果這個模塊你需要長期折騰的話,那么現(xiàn)在開始花點時間來清理它吧,之后會省掉很多煩惱。
● 你需要或者是你想引入上游的更新嗎?它是一個正在開發(fā)當(dāng)中的開源項目嗎?如果是的話,并且你想做改變的是上游的代碼,那么你不能對代碼有大的改動否則當(dāng)你每次pull代碼的時候都會經(jīng)歷一場merge的噩夢。所以你需要做一個友好的團(tuán)隊合作者,接受這個錯誤,將帶有你修正的代碼補丁發(fā)給代碼的維護(hù)者。
● 要做多少工作?你一天內(nèi)實際上能清理多少行代碼?我們估計多于100行,少于1000行,好,我們假設(shè)是1000行。所以如果一個模塊有30,000行代碼的話,你可能需要一個月的時間。你有那么多時間嗎?值得這么做么?
● 它是你核心的功能嗎?如果這個模塊只是邊緣的模塊,譬如字體渲染或者圖像渲染,你可能并不在意它是否是亂七八糟的。你可能全盤不要,將來用另外的東西來代替,誰知道呢。如果這段代碼關(guān)乎核心的性能,你需要慎重對待。
● 這段代碼有多糟糕?如果代碼僅僅有一點點糟糕,那么可能你還是可以忍受的。如果它是不可理喻的,令人崩潰的話,那么我們就必須對它下手了。
1. 建立測試用例
要認(rèn)真清理一段代碼意味著花一段時間來徹底清理它。你可能會毀壞它們。
如果你有一個比較好的測試用例,有一定的覆蓋率,你將會很容易知道什么已經(jīng)損壞了,并且你能夠很快的知道你犯了什么愚蠢的錯誤。想要節(jié)省建立測試用例的時間在整個的清理代碼的過程中是可笑的。建立測試用例吧。這是你第一件需要做的事情。
單元測試是最好的,但是所有的代碼并不適應(yīng)單元測試。如果單元測試過于繁瑣,就換用集成測試吧。譬如,一個游戲關(guān)卡中需要一個人物完成一系列的動作和你清理的代碼有關(guān)。
這樣的測試更加耗時,所以不可能在每一次更改之后都測試一次,雖然這是最理想的情況。因為你將每一次改變都放到了版本控制系統(tǒng)中,所以情況還不是那么糟糕。所以每一段時間(比如,五個更改)就測試一次。當(dāng)你發(fā)現(xiàn)了一個問題時,你可以通過二進(jìn)制搜尋最近的幾次commit中找到什么地方導(dǎo)致了問題的發(fā)生。
如果你發(fā)現(xiàn)了測試沒有發(fā)現(xiàn)的問題,確保將這個也加入到測試中,以便將來可以測試它。
2. 使用代碼版本控制系統(tǒng)
還有人需要被告知要使用代碼版本控制系統(tǒng)嗎?我希望沒有。
清理工作是很關(guān)鍵的。你可能要做很多很多小的修改。如果什么地方出錯了,你想回顧版本歷史,你可能找到它錯在哪。
如果你和我一樣,你可能有時重構(gòu)(清理愚蠢的類)的時候會出錯,并且后來意識到這并不是個好的點子,或者這是個好點子,但是如果先做了什么之后所有的一切會變得更簡單。所以你想快速的恢復(fù)一切到原狀并且重新開始。
你的公司應(yīng)該已經(jīng)有代碼控制系統(tǒng)了,你可以在不同的分支進(jìn)行修改,在不打擾別人的情況下隨意的commit。
就算情況不是這樣的,你也應(yīng)該使用版本控制。下載Mercurial(或Git),創(chuàng)建新的倉庫,將代碼從你們公司的愚蠢的系統(tǒng)中簽出并放在這里。在庫中commit你的更改。當(dāng)你完成了之后你可以將所有的一切merge到那愚蠢的系統(tǒng)中。
拷貝庫到一個代碼控制系統(tǒng)中僅僅需要幾分鐘。很值得這么做。如果你不懂Mercurial,花一個小時學(xué)習(xí)它。你會為你這么做感到高興的。如果你愿意的話,花30個小時學(xué)習(xí)下Git(我是開玩笑的!并不用這么久。現(xiàn)在是“nerd”戰(zhàn)斗的時候了!)
3. 每次僅僅做一個小小的改動
有兩種方法改進(jìn)壞的代碼:革命和改革。革命是用火把一切都燒掉,從新寫一遍。改革是在不破壞的基礎(chǔ)上每次只進(jìn)行一點小小的改變。
這篇文章是關(guān)于改革的方法。我不是說革命的方法從來不是必要的。有時代碼太糟糕了,需要用革命的方法。但是那些覺得改革的進(jìn)度太慢的人們往往會鼓勵改革,然而經(jīng)常沒有意識到問題的復(fù)雜性,并最終并沒有比現(xiàn)存的系統(tǒng)更好。
Joel Spolsky寫過一篇經(jīng)典的文章,他沒有掉入到這個緊張的爭論的陷阱中。
改革的最好的方法就是一次只做一個小的改變,測試它,并且commit它。當(dāng)一個改變很小時,它更容易理解改動的后果以及確保改動不會影響現(xiàn)有的功能。如果什么地方出錯了,你僅僅需要核查很少的一部分代碼。
如果你開始做更改并且意識到改得很糟糕,那么你恢復(fù)到上一次的commit,不會損失太多的無用功。如果你過了一段時間才發(fā)現(xiàn)什么地方有細(xì)微的差錯,你可以在版本歷史中使用二進(jìn)制搜找到導(dǎo)致問題的更改。
最常見的錯誤就是一次進(jìn)行多處更改。譬如,當(dāng)去除不必要的類層次的勢后,你發(fā)現(xiàn)API的方法并不是像你喜歡的使用方法,而你打算重新組織它們。不要這么做!先去除層次結(jié)構(gòu),commit之后再更改API。
聰明的程序員懂得組織,所以他們也不需要太聰明。
試著找一個途徑,沿著這個途徑你可以把代碼變成你想要的模樣,每次只有一點點改動。譬如,第一步你重命名方法,使之名字更合理。下一步,你可以將成員變量變成方法的參數(shù)。然后將算法變得更清楚些,等等。
如果你開始做更改,并且發(fā)現(xiàn)比你原先設(shè)想的改變要大,不要害怕又退回去,使用更小的更簡單的步驟去完成同樣的事情.。
4. 不要同時清理代碼和修正代碼
這是(3)的結(jié)果,但是仍然很重要。
這是一個常見的問題。你開始察看一個模塊,是因為你想加入某個新功能。然后你發(fā)現(xiàn)這個代碼相當(dāng)?shù)脑愀猓阅汩_始重新組織它并且加入新的功能。
問題在于清理代碼和修正錯誤是完全不同的目標(biāo)。當(dāng)你清理的勢后,你想讓代碼看起來更好,而沒有改變它的功能。當(dāng)你修正錯誤時, 你想改變功能。如果你同時清理代碼和改正錯誤,很難保證清理不會改變什么。
先清理代碼,然后再在一個干凈的基礎(chǔ)上,加入新的功能。
- 刪除你沒有使用的功能
清理的時間正比于代碼的數(shù)量,復(fù)雜性和糟糕的程度。
如果代碼的功能你目前沒有使用,而且在可預(yù)見的將來也不會使用,那么就刪除它,這會減少你瀏覽的代碼數(shù),降低復(fù)雜度(刪除不必要的概念和依賴)。你會清理的更快的,而且最后的結(jié)果會更簡單。
不要留著代碼僅僅因為“誰知道呢,你可能某一天需要它”。代碼是有代價的 – 它需要被移植,修正錯誤,被閱讀以及被理解。你有更少的代碼,就更好。就算在最不可能的情況下,你需要這個舊代碼,你也能從代碼庫中找到它。
6. 刪除大部分的注釋
爛代碼很少會有好的注釋。它們通常是這樣的:
- // Pointless:
- // Set x to 3
- x = 3;
- // Incomprehensible:
- // Fix for CB (aug)
- pos += vector3(0, -0.007, 0);
- // Sowing fear and doubt:
- // Really we shouldn't be doing this
- t = get_latest_time();
- // Downright lying:
- // p cannot be NULL here
- p->set_speed(0.7);
看看整個代碼。如果一個注釋對你來說不再有意義,也對你理解代碼沒什么幫助,那么就刪除它。否則你只會浪費你的腦力去理解一堆對你理解代碼沒幫助的注釋。
同樣的刪除那些已經(jīng)被注釋掉的代碼。如果你還需要它的時候,它還在你的代碼倉庫中。
甚至如果注釋是正確而且有用的,記住你還可以重構(gòu)你的代碼。可能當(dāng)你完成重構(gòu)后,這些注釋不再正確了。這個世界上還沒有一個單元測試能夠告訴你注釋是否已經(jīng)損壞了。
好代碼需要很少的注釋因為代碼自己已經(jīng)自說明了而且很容易理解。擁有好名字的變量不需要注釋去解釋它們的用途。函數(shù)如果有好的輸入輸出,沒有特殊情況時是不需要說明的。簡單的寫得很好的算法在沒有注釋的情況下也是容易理解的。而斷言記錄了條件和預(yù)測。
大部分情況下,最好的做法是刪除所有舊的注釋,專注于讓代碼變得干凈和具有可讀性,然后再在需要的地方添加代碼 – 這些注釋反應(yīng)新的API的用途以及你對代碼的理解。
7. 避免共享的可更改的狀態(tài)
共享的可更改的狀態(tài)是理解代碼的最大阻礙,因為它允許隔一段距離的行動,一段代碼可以改變另一段完全不同的代碼的行為。人們常說多線程是困難的。事實上,是由于線程共享了可更改的狀態(tài),才導(dǎo)致了問題。如果你能避免它們的話,多線程并不復(fù)雜。
如果你的目標(biāo)是寫高性能的軟件,你應(yīng)該不能避免一切可更改的狀態(tài),但是你的代碼仍然可以從減少它而獲益。為了“大部分功能完善”而努力吧,確保你確切的知道什么狀態(tài)在什么地方改變了,并且知道原因。
共享的可更改的狀態(tài)來自不同的地方:
● 全局變量。最經(jīng)典的例子。現(xiàn)在每個人都知道全局變量的壞處。但是要注意(有時人們會忘記),全局變量是唯一的會造成問題的共享的可更改狀態(tài)。全局常量并不糟糕,Sprintf也不糟糕。
● 對象 – 裝有樂趣的大袋子。對象能夠集合很多方法,無疑可以共享很多可變的狀態(tài)(成員)。如果一個懶惰的程序員需要將一些信息在方法之間傳遞的話,她可以建立一個新成員,所以可以依照需要來讀它和寫它。這非常像全局變量。多么有意思!當(dāng)一個對象有越來越多的成員時,問題就越來越嚴(yán)重。
● 巨大的函數(shù)。你可能已經(jīng)聽說它們了。這種神秘的產(chǎn)物棲息在最黑暗的代碼洞穴的最底層。心眼壞的程序員在陰暗的酒吧里談?wù)撍鼈儯麄兊睦碇潜凰麄冇鲆姷拇a摧毀了:“我不停地向下翻向下翻,我不能相信自己的眼睛。居然有12,000行。”當(dāng)函數(shù)足夠長的時候,它們本地變量將和全局變量一樣糟糕。我們不可能知道改變2000行之后的一個局部變量會有什么效果。
● 引用和指針參數(shù)。引用和指針參數(shù)沒有被聲明為const被傳進(jìn)函數(shù)時,可以在被調(diào)用者,調(diào)用者以及任何能被傳遞相同的指針的對象之間充當(dāng)共享的可變的狀態(tài)。
這里有一些避免共享的可更改的狀態(tài)的建議:
- 將較大的函數(shù)切分成較小的函數(shù)。
- 將較大的對象切分成較小的變量,將相關(guān)的成員放在一起。
- 將成員變成private。
- 將函數(shù)聲明const,返回結(jié)果,而不是可更改的狀態(tài)。
- 將函數(shù)聲明static,從參數(shù)獲得值,而不是從共享狀態(tài)那里取值。
- 避免完全使用對象,實現(xiàn)純凈的功能,不要引入副作用。
- 將本地變量聲明const。
- 將指針和引用聲明const。
8. 避免不必要的復(fù)雜性
不必要的復(fù)雜性通常是過度工程化的結(jié)果 – 支持的結(jié)構(gòu)(如序列化,引用計數(shù)器,虛擬接口,抽象工廠,訪問者等等)會拖慢真正有實際功能的代碼。
有時候過工程化是因為一些項目開始的時候有一些更大的野心,多于實際完成的。更多的情況,我想是因為程序員讀了關(guān)于設(shè)計模式的書之后和瀑布模型之后的想法,他認(rèn)為過工程化會形成更“堅固”和“高質(zhì)量”的產(chǎn)品。
通常,這個笨重的,僵化的,過度復(fù)雜的模型不能適應(yīng)功能需求,而這是設(shè)計師不期望的。那些功能可能之后用hack的方式來實現(xiàn),成了在象牙塔最頂上的螺栓和后門,變成了神經(jīng)錯亂的混合結(jié)構(gòu)。
治愈過度工程化的方法就是YAGNI(you are not gonna need it)-你不需要它!只有當(dāng)需要一個東西的時候才建造它。當(dāng)你需要它的時候才建立更復(fù)雜的東西,而不是在你需要之前。
避免不必要的復(fù)雜性的一些實際的方法:
- 移除你沒有用到的東西(就像上面建議的一樣)。
- 簡化必要的概念,避免不必要的概念。
- 移除不必要的抽象,用實際的實現(xiàn)來替代。
-
移除不必要的虛擬化,并且簡化對象的結(jié)構(gòu)。
如果一個設(shè)置曾經(jīng)使用過,那么就避免在用另外的配置來運行這個模塊。
9. 就這么多了
現(xiàn)在開始清理你的“房間”吧!