高效清理爛代碼的 10 個建議
猜猜看怎麼了!你接手了一堆混亂的舊代碼。恭喜你!如今都是你的了。混亂的代碼可能來自任何地方——中間件、網絡、可能來自你本身的公司。
你知道在一個角落裏有一個傢伙,沒有人過去管他在作什麼。猜猜看他一直在作什麼?辛辛苦苦寫出了代碼,倒是一堆爛代碼。
你還記得這個模塊是一個傢伙幾年前寫的,在他離開公司以前。這個模塊已經有20個不一樣的人加過補丁,進行過代碼修復,並且他們也並不理解代碼究竟是作了什麼。是的,就是這樣的代碼。
或者你從網上下載下的開源的軟件,你知道它很是的可怕,可是它解決了一個很是專的而且對你來講很是棘手的問題,解決這個問題你可能要花上幾年。
爛代碼不必定是問題,只要它們沒有出錯,沒有人會對它嗤之以鼻。但不幸的是,它們沒被發現的機率過小了。錯誤會被發現。須要新的功能,新系統發佈了。如今你不得不面對這堆恐怖的代碼,試着去清理它們。這篇文章爲這種不幸的狀況提供了一些建議。
1. 值得清理麼?
第一件你須要問問本身的事情就是代碼值得清理麼。我不是說當問到是否要清理代碼時,你必定要回答是或者必定回答不是。是你對代碼負有責任,也是你須要一直面對它們直到最終寫出的代碼是你樂意維護的,也是你很自豪的放入代碼庫的。
若是你以爲就算代碼看起來很可怕,也不值得浪費你原本就很緊張的時間來修復它們。因此你僅僅作了最最微小的調整解救燃眉之急。
換句話說,你也能夠將代碼看做本身的,也能夠看做是別人的。
兩種狀況都有優缺點。優秀的程序員看到爛代碼時會以爲很難受。他們會拿出火把和叉子而且高呼:「太亂了,太亂了」。這是一種優秀的品質。
可是清理代碼是一個繁雜的工做。很容易就低估了時間。甚至有時候和從頭開始寫代碼同樣的耗時。而且短時間並無帶來任何的短時間效應。兩個星期的時間清理代碼並不會帶來任何新的功能,但有可能引入一些新的錯誤。
另外一方面,若是長時間不清理代碼可能會帶來災難性的毀滅。混亂是代碼的殺手。
因此,這並非一個容易作出的決定。須要考慮一些事情:
- 你指望對這段代碼作多少改變?你是但願僅僅修改這個小錯誤呢,仍是這段代碼還要使用屢次,因此你但願將它「調教」的好些,而且加上新的功能。若是僅僅是修復一個錯誤,那麼最好是別打草驚蛇。然而,若是這個模塊你須要長期折騰的話,那麼如今開始花點時間來清理它吧,以後會省掉不少煩惱。
- 你須要或者是你想引入上游的更新嗎?它是一個正在開發當中的開源項目嗎?若是是的話,而且你想作改變的是上游的代碼,那麼你不能對代碼有大的改動不然當你每次pull代碼的時候都會經歷一場merge的噩夢。因此你須要作一個友好的團隊合做者,接受這個錯誤,將帶有你修正的代碼補丁發給代碼的維護者。
- 要作多少工做?你一天內實際上能清理多少行代碼?咱們估計多於100行,少於1000行,好,咱們假設是1000行。因此若是一個模塊有30,000行代碼的話,你可能須要一個月的時間。你有那麼多時間嗎?值得這麼作麼?
- 它是你核心的功能嗎?若是這個模塊只是邊緣的模塊,譬如字體渲染或者圖像渲染,你可能並不在乎它是不是亂七八糟的。你可能全盤不要,未來用另外的東西來代替,誰知道呢。若是這段代碼關乎核心的性能,你須要慎重對待。
- 這段代碼有多糟糕?若是代碼僅僅有一點點糟糕,那麼可能你仍是能夠忍受的。若是它是不可理喻的,使人崩潰的話,那麼咱們就必須對它下手了。
2. 創建測試用例
要認真清理一段代碼意味着花一段時間來完全清理它。你可能會毀壞它們。
若是你有一個比較好的測試用例,有必定的覆蓋率,你將會很容易知道什麼已經損壞了,而且你可以很快的知道你犯了什麼愚蠢的錯誤。想要節省創建測試用例的時間在整個的清理代碼的過程當中是好笑的。創建測試用例吧。這是你第一件須要作的事情。
單元測試是最好的,可是全部的代碼並不適應單元測試。若是單元測試過於繁瑣,就換用集成測試吧。譬如,一個遊戲關卡中須要一我的物完成一系列的動做和你清理的代碼有關。
這樣的測試更加耗時,因此不可能在每一次更改以後都測試一次,雖然這是最理想的狀況。由於你將每一次改變都放到了版本控制系統中,因此狀況還不是那麼糟糕。因此每一段時間(好比,五個更改)就測試一次。當你發現了一個問題時,你能夠經過二進制搜尋最近的幾回commit中找到什麼地方致使了問題的發生。
若是你發現了測試沒有發現的問題,確保將這個也加入到測試中,以便未來能夠測試它。
3. 使用代碼版本控制系統
還有人須要被告知要使用代碼版本控制系統嗎?我但願沒有。
清理工做是很關鍵的。你可能要作不少不少小的修改。若是什麼地方出錯了,你想回顧版本歷史,你可能找到它錯在哪。
若是你和我同樣,你可能有時重構(清理愚蠢的類)的時候會出錯,而且後來意識到這並非個好的點子,或者這是個好點子,可是若是先作了什麼以後全部的一切會變得更簡單。因此你想快速的恢復一切到原狀而且從新開始。
你的公司應該已經有代碼控制系統了,你能夠在不一樣的分支進行修改,在不打擾別人的狀況下隨意的commit。
就算狀況不是這樣的,你也應該使用版本控制。下載Mercurial(或Git),建立新的倉庫,將代碼從大家公司的愚蠢的系統中籤出並放在這裏。在庫中commit你的更改。當你完成了以後你能夠將全部的一切merge到那愚蠢的系統中。
拷貝庫到一個代碼控制系統中僅僅須要幾分鐘。很值得這麼作。若是你不懂Mercurial,花一個小時學習它。你會爲你這麼作感到高興的。若是你願意的話,花30個小時學習下Git(我是開玩笑的!並不用這麼久。如今是「nerd」戰鬥的時候了!)
4. 每次僅僅作一個小小的改動
有兩種方法改進壞的代碼:革命和改革。革命是用火把一切都燒掉,重新寫一遍。改革是在不破壞的基礎上每次只進行一點小小的改變。
這篇文章是關於改革的方法。我不是說革命的方法歷來不是必要的。有時代碼太糟糕了,須要用革命的方法。可是那些以爲改革的進度太慢的人們每每會鼓勵改革,然而常常沒有意識到問題的複雜性,並最終並無比現存的系統更好。
Joel Spolsky寫過一篇經典的
文章
,他沒有掉入到這個緊張的爭論的陷阱中。
改革的最好的方法就是一次只作一個小的改變,測試它,而且commit它。當一個改變很小時,它更容易理解改動的後果以及確保改動不會影響現有的功能。若是什麼地方出錯了,你僅僅須要覈查不多的一部分代碼。
若是你開始作更改而且意識到改得很糟糕,那麼你恢復到上一次的commit,不會損失太多的無用功。若是你過了一段時間才發現什麼地方有細微的差錯,你能夠在版本歷史中使用二進制搜找到致使問題的更改。
最多見的錯誤就是一次進行多處更改。譬如,當去除沒必要要的類層次的勢後,你發現API的方法並非像你喜歡的使用方法,而你打算從新組織它們。不要這麼作!先去除層次結構,commit以後再更改API。
聰明的程序員懂得組織,因此他們也不須要太聰明。
試着找一個途徑,沿着這個途徑你能夠把代碼變成你想要的模樣,每次只有一點點改動。譬如,第一步你重命名方法,使之名字更合理。下一步,你能夠將成員變量變成方法的參數。而後將算法變得更清楚些,等等。
若是你開始作更改,而且發現比你原先設想的改變要大,不要懼怕又退回去,使用更小的更簡單的步驟去完成一樣的事情.。
5. 不要同時清理代碼和修正代碼
這是(3)的結果,可是仍然很重要。
這是一個常見的問題。你開始察看一個模塊,是由於你想加入某個新功能。而後你發現這個代碼至關的糟糕,因此你開始從新組織它而且加入新的功能。
問題在於清理代碼和修正錯誤是徹底不一樣的目標。當你清理的勢後,你想讓代碼看起來更好,而沒有改變它的功能。當你修正錯誤時, 你想改變功能。若是你同時清理代碼和改正錯誤,很難保證清理不會改變什麼。
先清理代碼,而後再在一個乾淨的基礎上,加入新的功能。
6. 刪除你沒有使用的功能
清理的時間正比於代碼的數量,複雜性和糟糕的程度。
若是代碼的功能你目前沒有使用,並且在可預見的未來也不會使用,那麼就刪除它,這會減小你瀏覽的代碼數,下降複雜度(刪除沒必要要的概念和依賴)。你會清理的更快的,並且最後的結果會更簡單。
不要留着代碼僅僅由於「誰知道呢,你可能某一天須要它」。代碼是有代價的 – 它須要被移植,修正錯誤,被閱讀以及被理解。你有更少的代碼,就更好。就算在最不可能的狀況下,你須要這個舊代碼,你也能從代碼庫中找到它。
7. 刪除大部分的註釋
爛代碼不多會有好的註釋。它們一般是這樣的:
- // 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);
看看整個代碼。若是一個註釋對你來講再也不有意義,也對你理解代碼沒什麼幫助,那麼就刪除它。不然你只會浪費你的腦力去理解一堆對你理解代碼沒幫助的註釋。
一樣的刪除那些已經被註釋掉的代碼。若是你還須要它的時候,它還在你的代碼倉庫中。
甚至若是註釋是正確並且有用的,記住你還能夠重構你的代碼。可能當你完成重構後,這些註釋再也不正確了。這個世界上尚未一個單元測試可以告訴你註釋是否已經損壞了。
好代碼須要不多的註釋由於代碼本身已經自說明了並且很容易理解。擁有好名字的變量不須要註釋去解釋它們的用途。函數若是有好的輸入輸出,沒有特殊狀況時是不須要說明的。簡單的寫得很好的算法在沒有註釋的狀況下也是容易理解的。而斷言記錄了條件和預測。
大部分狀況下,最好的作法是刪除全部舊的註釋,專一於讓代碼變得乾淨和具備可讀性,而後再在須要的地方添加代碼 – 這些註釋反應新的API的用途以及你對代碼的理解。
8. 避免共享的可更改的狀態
共享的可更改的狀態是理解代碼的最大阻礙,由於它容許隔一段距離的行動,一段代碼能夠改變另外一段徹底不一樣的代碼的行爲。人們常說多線程是困難的。事實上,是因爲線程共享了可更改的狀態,才致使了問題。若是你能避免它們的話,多線程並不複雜。
若是你的目標是寫高性能的軟件,你應該不能避免一切可更改的狀態,可是你的代碼仍然能夠從減小它而獲益。爲了「大部分功能完善」而努力吧,確保你確切的知道什麼狀態在什麼地方改變了,而且知道緣由。
共享的可更改的狀態來自不一樣的地方:
- 全局變量。最經典的例子。如今每一個人都知道全局變量的壞處。可是要注意(有時人們會忘記),全局變量是惟一的會形成問題的共享的可更改狀態。全局常量並不糟糕,Sprintf也不糟糕。
- 對象。對象可以集合不少方法,無疑能夠共享不少可變的狀態(成員)。若是一個懶惰的程序員須要將一些信息在方法之間傳遞的話,她能夠創建一個新成員,因此能夠依照須要來讀它和寫它。這很是像全局變量。多麼有意思!當一個對象有愈來愈多的成員時,問題就愈來愈嚴重。
- 巨大的函數。你可能已經據說它們了。這種神祕的產物棲息在最黑暗的代碼洞穴的最底層。心眼壞的程序員在陰暗的酒吧裏談論它們,他們的理智被他們碰見的代碼摧毀了:「我不停地向下翻向下翻,我不能相信本身的眼睛。竟然有12,000行。」當函數足夠長的時候,它們本地變量將和全局變量同樣糟糕。咱們不可能知道改變2000行以後的一個局部變量會有什麼效果。
- 引用和指針參數。引用和指針參數沒有被聲明爲const被傳進函數時,能夠在被調用者,調用者以及任何能被傳遞相同的指針的對象之間充當共享的可變的狀態。
這裏有一些避免共享的可更改的狀態的建議:
- 將較大的函數切分紅較小的函數。
- 將較大的對象切分紅較小的變量,將相關的成員放在一塊兒。
- 將成員變成private。
- 將函數聲明const,返回結果,而不是可更改的狀態。
- 將函數聲明static,從參數得到值,而不是從共享狀態那裏取值。
- 避免徹底使用對象,實現純淨的功能,不要引入反作用。
- 將本地變量聲明const。
- 將指針和引用聲明const。
9. 避免沒必要要的複雜性
沒必要要的複雜性一般是過分工程化的結果 – 支持的結構(如序列化,引用計數器,虛擬接口,抽象工廠,訪問者等等)會拖慢真正有實際功能的代碼。
有時候過工程化是由於一些項目開始的時候有一些更大的野心,多於實際完成的。更多的狀況,我想是由於程序員讀了關於設計模式的書以後和瀑布模型以後的想法,他認爲過工程化會造成更「堅固」和「高質量」的產品。
一般,這個笨重的,僵化的,過分複雜的模型不能適應功能需求,而這是設計師不指望的。那些功能可能以後用hack的方式來實現,成了在象牙塔最頂上的螺栓和後門,變成了神經錯亂的混合結構。
治癒過分工程化的方法就是YAGNI(you are not gonna need it)-你不須要它!只有當須要一個東西的時候才建造它。當你須要它的時候才創建更復雜的東西,而不是在你須要以前。
避免沒必要要的複雜性的一些實際的方法:
- 移除你沒有用到的東西(就像上面建議的同樣)。
- 簡化必要的概念,避免沒必要要的概念。
- 移除沒必要要的抽象,用實際的實現來替代。
- 移除沒必要要的虛擬化,而且簡化對象的結構。
- 若是一個設置曾經使用過,那麼就避免在用另外的配置來運行這個模塊。
10. 就這麼多了
如今開始清理你的「房間」吧!
原文連接:
Niklas Frykholm
翻譯:
伯樂在線
-
唐小娟
譯文連接:
http://blog.jobbole.com/28672/
歡迎關注本站公眾號,獲取更多信息