1. 程式人生 > >重構:一項常常被忽略的基本功

重構:一項常常被忽略的基本功

摘要: 每一個程式設計師都應該讀的一本書。

Fundebug經授權轉載,版權歸原作者所有。

五月初的時候朋友和我說《重構》出第 2 版了,我才興沖沖地下單,花了一個禮拜時間一口氣把它讀完後,才有了這篇書評。掩卷沉思,我無比贊同豆瓣網友“天心一”的評論:

這本書雖然很流行,但是應該看它而沒有看的人,還是太多太多了。

一個老讀者的自白

作為一個開發者,2012年初識本書的時候,我在寫 Java;2019年本書再版,我在寫 JavaScript。真是應了那句老話兒:“凡是可以用 JavaScript 來寫的應用,最終都會用 JavaScript 來寫。”

JavaScript 特別適合重構,因為它很容易寫的無法維護。

當然這只是個玩笑,實際上作者也解釋過:重構背後的理念和架構適用於任何程式語言,選擇 JavaScript 只是因為它應用的比較廣泛。無論使用哪種程式語言都可以寫出優秀的或者糟糕的程式碼,同樣也都可以以本書的思路和技巧進行重構。

使用 JavaScript 展示程式碼範例,並不意味這本書中介紹的技巧只適用於JavaScript。

對比新舊兩版,作者“重構”了這本書:前幾章有所擴充套件,後幾章結構調整較大,移除了原來的 12-14 章。總的來說,重構後的第 2 版更接地氣、更適應時代:不再有“大型重構”,更多地聚焦操作的細節。

“Fowler 先生不僅沒有拔高,反而把功夫做得更紮實了。” —— 摘自譯者序

雖然本書的副標題是“改善既有程式碼的設計”,但通讀全書之後,我覺得這本書對於設計新系統時如何避免“壞味道”也是很有指導意義的。

重構和敏捷開發是一對親兄弟

提重構就不能不提敏捷開發,馬丁·福勒本身就是敏捷開發的發起者之一。敏捷作為“當紅炸子雞”,與重構有著很多相似的地方。

一是,這兩者都容易成為“掛羊頭,賣狗肉”中的“羊頭”,很多情況下,所謂的重構就是抽出時間來重寫現有的幾乎無法維護的程式碼,就如同很多“敏捷”只做到了“不拒絕需求變更”而沒有真正做到響應變化;二是,它們實現起來都是一定難度且它們的實踐過程可以是交叉的——它們都著眼於具體細節而不是空架子,都歡迎變化,都強調小步快走、持續改進;三是,敏捷開發很重要的兩個環節就是設計與重構,兩者相輔相成,彼此互補,在實踐的過程中保持較強的適應力。

重構的技巧

可以說,我在重構過程中遇到的問題大多都能在本書中找到答案。

我們看看作者對重構的定義:

重構(名詞): 對軟體內部結構的一種調整,目的是在不改變軟體可觀察行為的前提下,提高其可理解性,降低其修改成本。

重構(動詞): 使用一系列重構手法,在不改變軟體可觀察行為的前提下,調整其結構。

為何重構、如何重構、重構的原則與手法,都可以在這本書中找到。從第 5 章起作者提供了多達 300 頁的重構名錄、60 餘項重構的具體技巧(老版本是 70 多項,新版本移除了大規模專案的重構)。我覺得這一份非常詳盡的重構手法清單更接近於字典,適合粗讀之後在用到的時候再具體查閱。

至於什麼時候能夠用到這份名錄,作者在第 3 章也有介紹:當代碼有了“壞味道”就可以著手進行重構了。所謂“壞味道”,我認為並非是一程不變的準則,而是需要根據團隊、專案、採用的技術棧等各方面綜合得出的一種無法定量描述的經驗。所以,作者用了“味道”這樣一種體驗來代指需要重構的地方。在作者列出的每種“壞味道”中,都給出了對應的重構手法。雖然作者羅列的 20 多種“壞味道”覆蓋面很廣,但是你和你的團隊仍然可以總結出自己的經驗來指導重構。實際上,與第 1 版相比,第 2 版中的“壞味道”增加了“神祕命名”“全域性資料”“迴圈語句”,刪除了“不完美的庫類”。

我認為本書最重要也最容易被忽略的章節就是第 4 章——構築測試體系。在第 4 章中,作者通過一個生產計劃的示例一步一步的構建了一個完整的單元測試體系。顯然,掌握單元測試是有一定成本的,這就導致有些開發者(尤其是前端領域)完全不注重單元測試。他們認為測試是QA的職責,自己只需要保證冒煙測試通過即可。然而反直覺的是,良好的單元測試不但是重構的先決條件和好幫手,而且能幫我們整理設計的思路,從而更好的寫出優秀的程式碼。因為在寫單元測試的時候,我們會假設自己是一個“程式碼破壞者”,思考如何破壞程式碼的執行、尋找那些可能出錯的邊界條件。單元測試的編寫和執行可以在寫完程式碼後進行,也可以在寫程式碼之前動手。先寫單元測試再寫程式碼的技巧叫作測試驅動開發(TDD),也是敏捷開發的基石之一。關於TDD的技藝,作者的好友 Kent Beck 專門寫了一本書,即《測試驅動開發》。

作者在第 1 章的示例中提到:“小步快走,程式碼永遠處於可工作狀態。”而且作者特意強調:“每當我要進行重構的時候,第一個步驟永遠相同:我得確保即將修改的程式碼擁有一組可靠的測試。”

對於單元測試,我有一點小小的心得可以與大家分享:**儘量編寫純函式。**純函式是沒有副作用的函式,給出同樣的引數值,純函式總是返回同樣的結果,它不依賴於引數以外的值。顯然,純函式更便於單元測試。

當然單元測試也不是萬能的,它不可能檢出所有的bug,而且單元測試集的覆蓋率也是一個見仁見智的指標,具體需要寫多少單元測試,覆蓋多少程式碼,都是需要我們在開發中結合實際情況自己權衡的。無論如何,單元測試一直是一中非常重要卻常常被忽視的技能。

另外,我在開發實踐中堅持一個“432”的原則,供大家參考:

  • 一個類包括註釋程式碼不要超過400行;
  • 一個純函式最好不要超過30行;
  • 函式內迴圈巢狀最多2層。

重構的現狀

有些朋友對“重構”是不支援甚至是深惡痛絕的。

  • 一部分開發者不願意把精力“浪費”在重構上

他們覺得重構是“給飛行中的飛機修引擎”,有可能出現很多問題卻帶不來多少拿得出手的成績;重構總是會在“不經意間”破壞原有功能,帶來的麻煩很多,投入與收益完全不成比例,也很少會是面試的重點,花精力在這上面實在是費力不討好。

  • 許多leader反對盲目重構

在創業公司裡基本不會有重構的呼聲,原因無須贅言;而在一些大企業裡,leader們也不是都喜歡重構,因為花時間重構意味著佔用了開發新功能的時間,在程式碼還能跑起來甚至看起來跑得還不錯的時候去重構無疑是畫蛇添足;與重構帶來的風險相比,重構帶來的好處就不是那麼有說服力了。

  • 大部分QA對重構持謹慎的質疑態度

程式碼的變動意味著需要進行迴歸測試,而敏捷當道的時代,每個迭代中QA的關注重點都在新功能上,能夠分配給迴歸測試的精力很有限,而在測試通過後的重構極有可能導致此次變更對QA不透明,無形中增加了上線的風險。

我認為以上幾種反對重構的場景都是不恰當的重構導致的。

大家只是越來越接納“重構”這個詞,因為這個詞聽起來很好,有一種積極應對變化的感覺,但真正在做的還是跟以前一樣,毫無規矩的修改。

在實踐中,重構的要求是很高的:它需要有足夠詳盡的單元測試,需要有持續整合的環境,需要隨時隨地在“小步伐地永遠讓程式碼處於可工作狀態”下去進行改善。正是因為許多專案的“重構”是在並不滿足以上條件也沒有經過成本估算、策略規劃的情況下進行的,自然很容易導致失敗。

  • 水土不服

實際上,還有一部分開發者雖然認識到了重構是提升程式碼質量的有效手段,是諸如“在當下努力工作,以免日後有更多的活兒”此類觀念的具現。然而在某種程度上說,這在當前996.icu大環境下是不適用的。關於這一點就只能見仁見智、自己衡量了。

沒有銀彈

最後,我想說一句: 沒有銀彈

重構和設計模式一樣,是對於最佳實踐的提煉,是一系列技巧的集合,它不是打通任督二脈的靈丹妙藥。如果你是一個有追求但卻從來沒有系統地瞭解過重構的程式設計師(當然我不相信世界上會有這種程式設計師),那你會發現,你在日常工作中不經意間已經用過了這本書中提到的各種重構手法。

重構是注重實踐的技藝,僅僅瞭解其理念而忽視實踐則有如摶沙作飯,白費心思;而企圖把它當做“萬金油”來解決所有問題也只會陷入不恰當重構的陷阱,最終得不償失。只有在合適的場景下恰當的實踐,才會實現