關於爛代碼的那些事(下)

1.改善可維護性

改善代碼質量是項大工程,要開始這項工程,從可維護性入手每每是一個好的開始,但也僅僅只是開始而已。git

1.1.重構的悖論

不少人把重構當作一種一次性運動,代碼實在是爛的無法改了,或者沒什麼新的需求了,就召集一幫人專門拿出來一段時間作重構。這在傳統企業開發中多少能生效,可是對於互聯網開發來講卻很難適應,緣由有兩個:程序員

  1. 互聯網開發講究快速迭代,若是要作大型重構,每每須要暫停需求開發,這個基本上很難實現。
  2. 對於沒有什麼新需求的項目,每每意味着項目自己已通過了發展期,即便作了重構也帶來不了什麼收益。

這就造成了一個悖論:一方面那些變動頻繁的系統更須要重構;另外一方面重構又會耽誤開發進度,影響變動效率。github

面對這種矛盾,一種方式是放棄重構,讓代碼質量天然降低,直到工程的生命週期結束,選擇放棄或者重來。在某些場景下這種方式確實是有效的,可是我並不喜歡:比起讓工程師不得不把天天的精力都浪費在毫無心義的事情上,爲何不作些更有意義的事呢?面試

1.2.重構step by step

1.2.1.開始以前

開始改善代碼的第一步是把IDE的重構快捷鍵設到一個順手的鍵位上,這一步很是重要:決定重構成敗的每每不是你的新設計有多麼牛逼,而是重構自己會佔用多少時間。算法

好比對於IDEA來講,我會把重構菜單設爲快捷鍵:
屏幕快照 2015-12-27 上午11.15.58sql

這樣在我想去重構的時候就能夠隨手打開菜單,而不是用鼠標慢慢去點,快捷鍵每次只能爲重構節省幾秒鐘時間,可是卻能明顯減小工程師重構時的心理負擔,後面會提到,小規模的重構應該跟敲代碼同樣屬於平常開發的一部分。數據庫

我把重構分爲三類:模塊內部的重構、模塊級別的重構、工程級別的重構。分爲這三類並非由於我是什麼分類強迫症,後面會看到對重構的分類對於重構的意義。編程

1.2.2.隨時進行模塊內部的重構

模塊內部重構的目的是把模塊內部的邏輯梳理清楚,而且把一個巨大無比的函數拆分紅可維護的小塊代碼。大部分IDE都提供了對這類重構的支持,相似於:安全

  • 重命名變量
  • 重命名函數
  • 提取內部函數
  • 提取內部常量
  • 提取變量

這類重構的特色是修改基本集中在一個地方,對代碼邏輯的修改不多而且基本可控,IDE的重構工具比較健壯,於是基本沒有什麼風險。性能優化

如下例子演示瞭如何經過IDE把一個冗長的函數作重構:

上圖的例子中,咱們基本依靠IDE就把一個冗長的函數分紅了兩個子函數,接下來就能夠針對子函數中的一些爛代碼作進一步的小規模重構,而兩個函數內部的重構也能夠用一樣的方法。每一次小規模重構的時間都不該該超過60s,不然將會嚴重影響開發的效率,進而致使重構被無盡的開發需求淹沒。

在這個階段須要對現有的模塊補充一些單元測試,以保證重構的正確。不過以個人經驗來看,一些簡單的重構,例如修改局部變量名稱,或者提取變量之類的重構,即便沒有測試也是基本可靠的,若是要在快速完成模塊內部重構和100%的單元測試覆蓋率中選一個,我可能會選擇快速完成重構。

而這類重構的收益主要是提升函數級別的可讀性,以及消除超大函數,爲將來進一步作模塊級別的拆分打好基礎。

1.2.3.一次只作一個較模塊級別的的重構

以後的重構開始牽扯到多個模塊,例如:

  • 刪除無用代碼
  • 移動函數到其它類
  • 提取函數到新類
  • 修改函數邏輯

IDE每每對這類重構的支持有限,而且偶爾會出一些莫名其妙的問題,(例如修改類名時一不當心把配置文件裏的常量字符串也給修改了)。

這類重構主要在於優化代碼的設計,剝離不相關的耦合代碼,在這類重構期間你須要建立大量新的類和新的單元測試,而此時的單元測試則是必須的了。

爲何要建立單元測試?

  • 一方面,這類重構由於涉及到具體代碼邏輯的修改,靠集成測試很難覆蓋全部狀況,而單元測試能夠驗證修改的正確性。
  • 更重要的意義在於,寫不出單元測試的代碼每每意味着糟糕的設計:模塊依賴太多或者一個函數的職責過重,想象一下,想要執行一個函數卻要模擬十幾個輸入對象,每一個對象還要模擬本身依賴的對象……若是一個模塊沒法被單獨測試,那麼從設計的角度來考慮,無疑是不合格的。

還須要囉嗦一下,這裏說的單元測試只對一個模塊進行測試,依賴多個模塊共同完成的測試並不包含在內-例如在內存裏模擬了一個數據庫,並在上層代碼中測試業務邏輯-這類測試並不能改善你的設計。

在這個期間還會寫一些過渡用的臨時邏輯,好比各類adapter、proxy或者wrapper,這些臨時邏輯的生存期可能會有幾個月到幾年,這些看起來沒什麼必要的工做是爲了控制重構範圍,例如:

 

 

1

2

3

4

5

6

class Foo {

  String foo() {

  ...

  }

}

 

 

若是要把函數聲明改爲

 

 

1

2

3

4

5

6

class Foo {

   boolean foo() {

   ...

   }

}

 

 

那麼最好經過加一個過渡模塊來實現:

 

 

1

2

3

4

5

6

7

class FooAdaptor {

    private Foo foo;

    boolean foo() {

       return foo.foo().isEmpty();

    }

}

 

 

這樣作的好處是修改函數時不須要改動全部調用方,爛代碼的特徵之一就是模塊間的耦合比較高,每每一個函數有幾十處調用,牽一髮而動全身。而一旦開始全面改造,每每就會把一次看起來很簡單的重構演變成幾周的大工程,這種大規模重構每每是不可靠的。

每次模塊級別的重構都須要精心設計,提早劃分好哪些是須要修改的,哪些是須要用兼容邏輯作過渡的。但實際動手修改的時間都不該該超過一天,若是超過一天就意味着此次重構改動太多,須要控制一下修改節奏了。

1.2.4.工程級別的重構不能和任何其餘任務並行

不安全的重構相對而言影響範圍比較大,好比:

  • 修改工程結構
  • 修改多個模塊

我更建議這類操做不要用IDE,若是使用IDE,也只使用最簡單的「移動」操做。這類重構單元測試已經徹底沒有做用,須要集成測試的覆蓋。不過也沒必要緊張,若是隻作「移動」的話,大部分狀況下基本的冒煙測試就能夠保證重構的正確性。

這類重構的目的是根據代碼的層次或者類型進行拆分,切斷循環依賴和結構上不合理的地方。若是不知道如何拆分,能夠依照以下思路:

  1. 優先按部署場景進行拆分,好比一部分代碼是公用的,一部分代碼是本身用的,能夠考慮拆成兩個部分。換句話說,A服務的修改能不能影響B服務。
  2. 其次按照業務類型拆分,兩個無關的功能能夠拆分紅兩個部分。換句話說,A功能的修改能不能影響B功能。
  3. 除此以外,儘可能控制本身的代碼潔癖,不要把代碼切成一大堆豆腐塊,會給往後的維護工做帶來不少沒必要要的成本。
  4. 案能夠提早review幾回,多參考一線工程師的意見,避免實際動手時才冒出新的問題。

而這類重構絕對不能跟正常的需求開發並行執行:代碼衝突幾乎沒法避免,而且會讓全部人崩潰。個人作法通常是在這類重構前先演練一次:把模塊按大體的想法拖來拖去,經過編譯器找到依賴問題,在平常上線中把容易處理的依賴問題解決掉;而後集中團隊裏的精英,通知全部人暫停開發,花最多二、3天時間把全部問題集中突擊掉,新的需求都在新代碼的基礎上進行開發。

若是歷史包袱實在過重,能夠把這類重構也拆成幾回作:先大致拆分紅幾塊,再分別拆分。不管如何,這類重構務必控制好變動範圍,一次嚴重的合併衝突有可能讓團隊中的全部人幾個周緩不過勁來。

1.3.重構的週期

典型的重構週期相似下面的過程:

  1. 在正常需求開發的同時進行模塊內部的重構,同時理解工程原有代碼。
  2. 在需求間隙進行模塊級別的重構,把大模塊拆分爲多個小模塊,增長腳手架類,補充單元測試,等等。
  3. (若是有必要,好比工程過於巨大致使常常出現相互影響問題)進行一次工程級別的拆分,期間須要暫停全部開發工做,而且此次重構除了移動模塊和移動模塊帶來的修改以外不作任何其餘變動。
  4. 重複一、2步驟

1.3.1.一些重構的tips

  1. 只重構常常修改的部分,若是代碼一兩年都沒有修改過,那麼說明改動的收益很小,重構能改善的只是可維護性,重構不維護的代碼不會帶來收益。
  2. 抑制住本身想要多改一點的衝動,一次失敗的重構對代碼質量改進的影響多是毀滅性的。
  3. 重構須要不斷的練習,相比於寫代碼來講,重構或許更難一些。
  4. 重構可能須要很長時間,有可能甚至會達到幾年的程度(我以前用斷斷續續兩年多的時間重構了一個項目),主要取決於團隊對於風險的容忍程度。
  5. 刪除無用代碼是提升代碼可維護性最有效的方式,切記,切記。
  6. 單元測試是重構的基礎,若是對單元測試的概念還不是很清晰,能夠參考使用Spock框架進行單元測試

2.改善性能與健壯性

2.1.改善性能的80%

性能這個話題愈來愈多的被人提起,隨便收到一份簡歷不寫上點什麼熟悉高併發、作過性能優化之類的彷佛都很差意思跟人打招呼。

說個真事,幾年前在我作某公司的ERP項目,裏面有個功能是生成一個報表。而使用咱們系統的公司裏有一我的,他天天要在下班前點一下報表,導出到excel,再發一封郵件出去。

問題是,那個報表每次都要2,3分鐘才能生成。

我當時正年輕氣盛,看到有個兩分鐘才能生成的報表一下就來了興趣,翻出了那段不知道誰寫的代碼,發現裏面用了3層循環,每次都會去數據庫查一次數據,再把一堆數據拼起來,一股腦塞進一個tableview裏。

面對這種代碼,我還能作什麼呢?

  • 我馬上把那個三層循環幹掉了,經過一個存儲過程直接輸出數據。
  • sql數據計算的邏輯也被我精簡了,一些不必作的外聯操做被我幹掉了。
  • 我還發現不少ctrl+v生成的無用的控件(那時仍是用的delphi),那些控件密密麻麻的貼在顯示界面上,只是被前面的大table擋住了,我固然也把這些玩意都刪掉了;
  • 打開界面的時候還作了一些雜七雜八的工做(好比去數據庫裏更新點擊數之類的),我把這些放到了異步任務裏。
  • 後面我又以爲不必每次打開界面都要加載全部數據(那個tableview有幾千行,幾百列!),因而我hack了默認的tableview,每次打開的時候先計算當前實際顯示了多少內容,把參數發給存儲過程,初始化只加載這些數據,剩下的再經過線程異步加載。

作了這些以後,界面只須要不到1s就能展現出來了,不過我要說的不是這個。

後來我去客戶公司給那個操做員演示新的模塊的時候,點一下,刷,數據出來了。那我的很驚恐的看着我,而後問我,是否是數據不許了。

再後來,我又加了一個功能,那個模塊每次打開以後都會顯示一個進度條,上面的標題是「正在校驗數據……」,進度條走完大概要1分鐘左右,我跟那人說校驗數據計算量很大,會比較慢。固然,實際上那60秒里程序毛事都沒作,只是在一點點的更新那個進度條(我還作了個彩蛋,在讀進度的時候按上上下下左右左右BABA的話就能夠加速10倍讀條…)。客戶很開心,說感受數據準確多了,固然,他沒發現彩蛋。

我寫了這麼多,是想讓你明白一個事實:大部分程序對性能並不敏感。而少數對性能敏感的程序裏,一大半能夠靠調節參數解決性能問題;最後那一小撮須要修改代碼優化性能的程序裏,性價比高的工做又是少數。

什麼是性價比?回到剛纔的例子裏,我作了那麼多事,每件事的收益是多少?

  • 把三層循環sql改爲了存儲過程,大概讓我花了一天時間,讓加載時間從3分鐘變成了2秒,模塊加載變成了」唰「的一下。
  • 後面的一坨事情大概花了我一週多時間,尤爲是hack那個tableview,讓我連週末都搭進去了。而全部的優化加起來,大概優化了1秒左右,這個數據是經過日誌查到的:即便是我本身,打開模塊也沒感受出有什麼明顯區別。

我如今遇到的不少面試者說程序優化時老是喜歡說一些玄乎的東西:調用棧、尾遞歸、內聯函數、GC調優……可是當我問他們:把一個普通函數改爲內聯函數是把原來運行速度是多少的程序優化成多少了,卻不多有人答出來;或者是扭扭捏捏的說,應該不少,由於這個函數會被調用不少遍。我再問會被調用多少遍,每遍是多長時間,就答不上來了。

因此關於性能優化,我有兩個觀點:

  1. 優化主要部分,把一次網絡IO改成內存計算帶來的收益遠大於捯飭編譯器優化之類的東西。這部份內容能夠參考Numbers you should know;或者本身寫一個for循環,作一個無限i++的程序,看看一秒鐘i能累加多少次,感覺一下cpu和內存的性能。
  2. 性能優化以後要有量化數據,明確的說出優化後哪一個指標提高了多少。若是有人由於」提高性能「之類的理由寫了一堆讓人沒法理解的代碼,請務必讓他給出性能數據:這頗有多是一坨沒有什麼收益的爛代碼。

至於具體的優化措施,無外乎幾類:

  1. 讓計算靠近存儲
  2. 優化算法的時間複雜度
  3. 減小無用的操做
  4. 並行計算

關於性能優化的話題還能夠講不少內容,不過對於這篇文章來講有點跑題,這裏就再也不詳細展開了。

2.2.決定健壯性的20%

前一陣聽一個技術分享,說是他們在編程的時候要考慮太陽黑子對cpu計算的影響,或者是農民伯伯的豬把基站拱塌了之類的特殊場景。若是要優化程序的健壯性,那麼有時候就不得不去考慮這些極端狀況對程序的影響。

大部分的人應該不用考慮太陽黑子之類的高深的問題,可是咱們須要考慮一些常見的特殊場景,大部分程序員的代碼對於一些特殊場景都會有或多或少考慮不周全的地方,例如:

  • 用戶輸入
  • 併發
  • 網絡IO

常規的方法確實可以發現代碼中的一些bug,可是到了複雜的生產環境中時,總會出現一些徹底沒有想到的問題。雖然我也想了好久,遺憾的是,對於健壯性來講,我並無找到什麼立竿見影的解決方案,所以,我只能謹慎的提出一點點建議:

  • 更多的測試測試的目的是保證代碼質量,但測試並不等於質量,你作覆蓋80%場景的測試,在20%測試不到的地方仍是有可能出問題。關於測試又是一個巨大的話題,這裏就先不展開了。
  • 謹慎發明輪子。例如UI庫、併發庫、IO client等等,在能知足要求的狀況下儘可能採用成熟的解決方案,所謂的「成熟」也就意味着經歷了更多實際使用環境下的測試,大部分狀況下這種測試的效果是更好的。

3.改善生存環境

看了上面的那麼多東西以後,你能夠想一下這麼個場景:

在你作了不少事情以後,代碼質量彷佛有了質的飛躍。正當你覺得終於能夠擺脫每天踩屎的日子了的時候,某次不當心瞥見某個類又長到幾千行了。

你憤怒的翻看提交日誌,想找出罪魁禍首是誰,結果卻發現天天都會有人往文件裏提交那麼十幾二十行代碼,每次的改動看起來都沒什麼問題,可是日積月累,一年年過去,當初花了九牛二虎之力重構的工程又成了一坨爛代碼……

任何一個對代碼有追求的程序員都有可能遇到這種問題,技術在更新,需求在變化,公司人員會流動,而代碼質量總會在不經意間偷偷的變差……

想要改善代碼質量,最後每每就會變成改善生存環境。

3.1.1.統一環境

團隊須要一套統一的編碼規範、統一的語言版本、統一的編輯器配置、統一的文件編碼,若是有條件最好能使用統一的操做系統,這能避免不少無心義的工做。

就好像最近渣浪給開發所有換成了統一的macbook,一晚上之間之前的不少問題都變得不是問題了:字符集、換行符、IDE之類的問題只要一個配置文件就解決了,再也不有各類稀奇古怪的代碼衝突或者不兼容的問題,也不會有人忽然提交上來一些編碼格式稀奇古怪的文件了。

3.1.2.代碼倉庫

代碼倉庫基本上已是每一個公司的標配,而如今的代碼倉庫除了儲存代碼,還能夠承擔一些團隊溝通、代碼review甚至工做流程方面的任務,現在這類開源的系統不少,像gitlab(github)、Phabricator這類優秀的工具都能讓代碼管理變得簡單不少。我這裏無心討論svn、git、hg仍是什麼其它的代碼管理工具更好,就算最近火熱的git在複雜性和集中化管理上也有一些問題,其實我是比較期待能有替代git的工具產生的,扯遠了。

代碼倉庫的意義在於讓更多的人可以得到和修改代碼,從而提升代碼的生命週期,而代碼自己的生命週期足夠持久,對代碼質量作的優化纔有意義。

3.1.3.持續反饋

大多數爛代碼就像癌症同樣,當爛代碼已經產生了能夠感受到的影響時,基本已是晚期,很難治好了。

所以提早發現代碼變爛的趨勢很重要,這類工做能夠依賴相似於checkstyle,findbug之類的靜態檢查工具,及時發現代碼質量下滑的趨勢,例如:

  1. 天天都在產生大量的新代碼
  2. 測試覆蓋率降低
  3. 靜態檢查的問題增多

有了代碼倉庫以後,就能夠把這種工具與倉庫的觸發機制結合起來,每次提交的時候作覆蓋率、靜態代碼檢查等工做,jenkins+sonarqube或者相似的工具就能夠完成基本的流程:伴隨着代碼提交進行各類靜態檢查、運行各類測試、生成報告並供人蔘考。

在實踐中會發現,關於持續反饋的五花八門的工具不少,可是真正有用的每每只有那麼一兩個,大部分人並不會去在每次提交代碼以後再打開一個網頁點擊「生成報告」,或者去登錄什麼系統看一下測試的覆蓋率是否是變低了,所以一個一站式的系統大多數狀況下會表現的更好。與其追求更多的功能,不如把有限的幾個功能整合起來,例如咱們把代碼管理、迴歸測試、代碼檢查、和code review集成起來,就是這個樣子:

固然,關於持續集成還能夠作的更多,篇幅所限,就很少說了。

3.1.4.質量文化

不一樣的團隊文化會對技術產生微妙的影響,關於代碼質量沒有什麼共同的文化,每一個公司都有本身的一套觀點,而且彷佛都能說得通。

對於我本身來講,關於代碼質量是這樣的觀點:

  1. 爛代碼沒法避免
  2. 爛代碼沒法接受
  3. 爛代碼能夠改進
  4. 好的代碼能讓工做更開心一些

如何讓大多數人認同關於代碼質量的觀點其實是有一些難度的,大部分技術人員對代碼質量的觀點是既不同意、也不反對的中立態度,而代碼質量就像是熵值同樣,放着無論老是會像更加混亂的方向演進,而且寫爛代碼的成本實在是過低了,以致於一個實習生花上一個禮拜就能夠毀了你花了半年精心設計的工程。

因此在提升代碼質量時,務必想辦法拉上團隊裏的其餘人一塊兒。雖然「引導團隊提升代碼質量」這件事情一開始會很辛苦,可是一旦有了一些支持者,而且有了能夠參考的模板以後,剩下的工做就簡單多了。

這裏推薦《佈道之道:引領團隊擁抱技術創新》這本書,裏面大部分的觀點對於代碼質量也是能夠借鑑的。僅靠喊口號很難讓其餘人寫出高質量的代碼,讓團隊中的其餘人體會到高質量代碼的收益,比喊口號更有說服力。

4.最後再說兩句

優化代碼質量是一件頗有意思,也頗有挑戰性的事情,而挑戰不光來自於代碼本來有多爛,要改進的也並不僅是代碼自己,還有工具、習慣、練習、開發流程、甚至團隊文化這些方方面面的事情。

寫這一系列文章前先後後花了半年多時間,一直處在寫一點刪一點的狀態:我自身關於代碼質量的想法和實踐也在經歷着不斷變化。我更但願能寫出一些可以實踐落地的東西,而不是喊喊口號,忽悠忽悠「敏捷開發」、「測試驅動」之類的幾個名詞就結束了。

可是在寫文章的過程當中就會慢慢發現,不少問題的改進方法確實不是一兩篇文章能夠說明白的,問題之間每每又相互關聯,全都展開說甚至超出了一本書的信息量,因此這篇文章也只能刪去了不少內容。

我參與過不少代碼質量很好的項目,也參與過一些質量很爛的項目,改進了不少項目,也放棄了一些項目,從最初的單打獨鬥本身改代碼,到後來帶領團隊優化工做流程,經歷了不少。不管如何,關於爛代碼,我決定引用一下《佈道之道》這本書裏的一句話:

「‘更好’,其實不是一個目的地,而是一個方向…在當前的位置和未來的目標之間,可能有不少至關不錯的地方。你只需關注離開如今的位置,而不要關心去向何方。」

相關文章
相關標籤/搜索