摘要: 每一個程式員都應該讀的一本書。
- 原文: 重構:一項常常被忽略的基本功
- 作者:hengg
經授權轉載,版權歸原作者所有。
五月初的時候朋友和我說《重構》出第 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大環境下是不适用的。關于這一點就隻能見仁見智、自己衡量了。
沒有銀彈
最後,我想說一句: 沒有銀彈。
重構和設計模式一樣,是對于最佳實踐的提煉,是一系列技巧的集合,它不是打通任督二脈的靈丹妙藥。如果你是一個有追求但卻從來沒有系統地了解過重構的程式員(當然我不相信世界上會有這種程式員),那你會發現,你在日常工作中不經意間已經用過了這本書中提到的各種重構手法。
重構是注重實踐的技藝,僅僅了解其理念而忽視實踐則有如抟沙作飯,白費心思;而企圖把它當做“萬金油”來解決所有問題也隻會陷入不恰當重構的陷阱,最終得不償失。隻有在合适的場景下恰當的實踐,才會實作其應有的價值。