近20年過去了病瞳,Martin Fowler先生終于推出了新版的《重構》山卦。本人有幸于ThoughtWorks技術雷達十周年峰會現(xiàn)場率先拿到了此書的國內發(fā)行版闷供。
在這20年中,軟件開發(fā)技術發(fā)生了很多重要的變化洼滚。新的編程語言不斷涌現(xiàn),老的編程語言也加快迭代技潘。主流編程語言大都支持了多種編程范式遥巴,函數(shù)式編程和面向對象一樣成了主流編程語言的標配千康。對并發(fā)的更好支持也已成為主流編程語言新的核心競爭力。于此同時各種軟件開發(fā)工具也日益現(xiàn)代化铲掐,常用的編程IDE都具備了面向重構拾弃、測試甚至容器化發(fā)布的自動化工具和快捷鍵。
基于上摆霉,很多人都認為新版的重構會迎合時代的變化豪椿,煥然一新。然而當我用一整天時間讀完全書后斯入,卻不禁如釋重負砂碉。正如本書中文譯者熊節(jié)先生所說“Flowler先生不僅沒有拔高,反而把功夫做的更扎實了”刻两。
確實增蹭,無論編程語言的語法如何變化、編程范型如何多元化磅摹、工具如何發(fā)展滋迈,軟件設計的目標并沒有變:那就在保證軟件滿足功能和非功能需求的前提下,如何更易應對變化以及更易讓人理解和維護户誓。由此所推導出來的軟件設計原則也是幾十年都沒有變饼灿,如高內聚、低耦合
帝美,如SOLID原則
等碍彭。甚至連GOF設計模式
至今依然生命力旺盛,除了偶有在一些新的編程范型中出現(xiàn)的新模式以及對原有模式的更簡單實現(xiàn)悼潭。此刻再回顧重構技術庇忌,它所傳授的如何識別代碼中的壞味道,以及如何采用小步安全的重構手法逐步將代碼演化到更易理解舰褪、更易應對變化的狀態(tài)皆疹,正是為了滿足軟件設計的核心訴求!所以重構應該和設計模式一樣占拍,是一項軟件開發(fā)中歷久而彌新的核心能力略就。
基于此,新版《重構》在主體內容上和第一版相似晃酒。首先從一個示例開始表牢,先讓讀者從整體上體會重構的過程和效果。然后給出了重構的具體概念和原則掖疮。之后Martin老先生給讀者列出了一份重要的代碼壞味道清單并逐一詮釋初茶。隨后用了一章篇幅來講述如何搭建對重構來說至關重要的“自動化測試體系”。最后Martin用整本書近四分之三的篇章詳細闡述了幾十種關鍵的重構手法。
而在所謂與時俱進的方面恼布,Martin則將更多的精力放在了對細節(jié)的持續(xù)優(yōu)化上螺戳。首先直接可見的是新版刪除了第一版中的最后幾章:“大型重構”、“重構折汞,復用與現(xiàn)實”倔幼,“重構工具”等,一方面是因為這幾章中有些內容在今天看來已不是那么重要爽待,其次所謂的“大型重構”其實仍舊是一系列小的重構手法的合理組合和持續(xù)應用损同。在第二版中Martin將重點放到了對重構手法的持續(xù)優(yōu)化上:首先他將原本22種代碼壞味道調整為24種,然后對所有重構手法進行了重新分類和排布鸟款,以便更加內聚和操作連貫膏燃。新版保留了第一版中大部分的手法,增加了一些更加具體和有用的手法何什,同時對所有的描述和示例都進行了更加精致的優(yōu)化组哩。
除這些之外,整本書的示例語言也從Java換成了JavaScript处渣。
接下來我們具體看一下新版中的種種變化伶贰。
JavaScript
不少人認為第二版中將重構的示例語言由Java變?yōu)镴avaScript,并不是一種妥當?shù)倪x擇罐栈。畢竟JavaScript作為一門動態(tài)腳本語言黍衙,并不適合較大規(guī)模的工業(yè)級軟件產品。但我個人認為選擇JavaScript是經過Martin深思熟慮的結果荠诬。
自從NodeJs將JavaScript帶進了服務端開發(fā)琅翻,JavaScript就變成了一門前后端通吃的語言。此外柑贞,ES6標準將JavaScript變成了一門現(xiàn)代語言望迎,語法上同時支持面向對象和函數(shù)式范式。最后JavaScript內置多種支持并發(fā)編程的特性凌外,它的包管理工具和生態(tài)也都建設得非常好。相比其它語言涛浙,JavaScript的受眾以及可應用的領域相對更加廣泛一些康辑。
JavaScript作為一門動態(tài)類型語言,IDE對其自動化重構的支持并沒有Java那么好轿亮。正因如此疮薇,書中介紹的手動重構步驟才會顯得更加有意義。而熟練掌握手動重構技巧我注,不僅能更好的理解重構的本質按咒,對在其它一些主流編程語言下實施重構也非常有價值(例如C、C++但骨、Python等)励七。
站在另一方面智袭,借助JavaScript的多范式編程以及動態(tài)類型的語法特性,很多重構手法可以做的更加簡單和靈活掠抬。例如JavaScript支持在函數(shù)內嵌套定義函數(shù)吼野,所以應用Extract Method
手法時,可以先把新提來出來的子函數(shù)定義在調用它的函數(shù)內部两波,這樣借助函數(shù)的閉包性可以減少函數(shù)間傳遞的參數(shù)數(shù)量瞳步,等到真正需要復用該子函數(shù)的時候再把它挪到外面。全書中類似的例子還有很多腰奋。
在書中单起,Martin使用了ES6的語法,不僅因為ES6標準為JavaScript引入了類和繼承的語法糖劣坊,而且在很多細節(jié)上也能讓代碼更加清晰嘀倒。例如書中對const
和let
關鍵字的大量使用等。但同時Martin也盡量避免使用其他程序員不太熟悉的JavaScript編程風格讼稚,例如JS獨特的對象模型以及generator
括儒、promise
和await/async
等語法。
因為使用JavaScript的緣故锐想,新版對“構筑測試體系”一章做了較大修改帮寻。Martin演示了如何使用JavaScript的測試框架Mocha
和斷言庫Chai
構筑自動化測試系統(tǒng),以支持重構的安全實施赠摇。
代碼壞味道
學好重構的關鍵在于掌握三個點:起點固逗、手法和目標(可以參考我之前寫的一篇文章:《高效重構(一):正確理解重構》)。
代碼的壞味道清單為我們指出了重構的起點藕帜。當發(fā)現(xiàn)代碼中有類似清單中所列的壞味道時烫罩,就暗示著這是一個應該實施重構的地方。由此可見代碼壞味道清單的重要性洽故。
Martin在新版中對這份清單進行了重新梳理贝攒。下表對比了兩版之間的差異。
第一版 | 第二版 | 變化概要 |
---|---|---|
? | 神秘命名Mysterious Name
|
新版增加时甚,突出了好的命名對于代碼的重要性 |
重復代碼Duplicataed Code
|
重復代碼Duplicataed Code
|
新版刪除了對”毫不相干的類中出現(xiàn)的重復“的描述隘弊,避免不合時宜的提取非本質重復 |
過長函數(shù)Long Method
|
過長函數(shù)Long Function
|
基本未變 |
過長參數(shù)列表Long Parameter List
|
過長參數(shù)列表Long Parameter List
|
新版提供了更多的重構解決方案 |
? | 全局數(shù)據(jù)Global Data
|
新版突出了全局數(shù)據(jù)對代碼耦合性的惡劣影響 |
? | 可變數(shù)據(jù)Mutable Data
|
新版突出了適宜的不可變性對代碼可維護性上的優(yōu)勢 |
發(fā)散式變化Divergent Change
|
發(fā)散式變化Divergent Change
|
新版提供了更多應對發(fā)散式變化的重構方案 |
霰彈式修改Shotgun Surgery
|
霰彈式修改Shotgun Surgery
|
新版提供了更多應對霰彈式修改的重構方案 |
依戀情結Feature Envy
|
依戀情結Feature Envy
|
新版對封裝單元的表述不再僅僅針對類,擴展到函數(shù)和模塊 |
數(shù)據(jù)泥團Data Clumps
|
數(shù)據(jù)泥團Data Clumps
|
基本未變 |
基本類型偏執(zhí)Primitive Obsession
|
基本類型偏執(zhí)Primitive Obsession
|
新版增加了”類字符類型變量“的危害荒适,并精簡了描述 |
Switch表達式Switch Statement
|
重復的SwitchRepleated Switch
|
新版承認第一版有些“矯枉過正”梨熙!這項重構的核心是消除重復的switch |
? | 循環(huán)語句Loops
|
凸顯了在函數(shù)式下,循環(huán)有了更多的更具語義性的表達方式 |
平行繼承體系Parallel Inheritance Hierarchies
|
? | 平行繼承體系其實是散彈式修改的特殊形式刀诬。為了不再突出面向對象咽扇,新版刪去 |
冗贅類Lazy Class
|
冗贅的元素Lazy Element
|
新版中體現(xiàn)了冗余的未必只是類,可能是函數(shù)、模塊等 |
夸夸其談通用性Speculative Generality
|
夸夸其談通用性Speculative Generality
|
基本未變 |
臨時字段Temporary Field
|
臨時字段Temporary Field
|
刪除”為了特殊算法引入臨時字段“的情況 |
過長的消息鏈Message Chains
|
過長的消息鏈Message Chains
|
基本未變 |
中間人Middle Man
|
中間人Middle Man
|
基本未變 |
狎昵關系Inappropriate Intimacy
|
內幕交易Insider Trading
|
新版中不再僅限于類之間的不恰當耦合质欲,將描述范圍擴大到模塊 |
過大的類Large Class
|
過大的類Large Class
|
舊版中描述的”GUI大類“已經有些過時了树埠,所以刪除了 |
異曲同工的類Alternative Classes with Different Interfaces
|
異曲同工的類Alternative Classes with Different Interfaces
|
新版對所謂類的"異曲同工"解釋的更加清晰 |
不完美的類庫Incomplete Library Class
|
? | 類庫屬于可擴展不可修改的代碼,新版中把對類庫的壞味道辨別和重構刪掉了 |
純數(shù)據(jù)類Data Class
|
純數(shù)據(jù)類Data Class
|
新版提出了例外情況:當純數(shù)據(jù)類不可修改且僅用于傳遞信息時 |
被拒絕的遺贈Refused Bequest
|
被拒絕的遺贈Refused Bequest
|
基本未變 |
注釋Comments
|
注釋Comments
|
基本未變 |
總結一下把敞,新版在”代碼的壞味道“這一章中除了一些語言上的調整外弥奸,主要的變化如下:
- 更加注重對細節(jié)的打磨。新增一些小的明確的壞味道(如
Mysterious Name
奋早、Global Data
)盛霎,去除了一些大的、稍顯模糊或者可被分解的壞味道(如Parallel Inheritance Hierarchies
耽装、Inappropriate Intimacy
)愤炸; - 修正了一些之前不準確的描述。例如將
Switch Statement
更改為Repleated Switch
掉奄。這個修改非常有意思规个,Martin在書中承認當年有些矯枉過正了!那時多少人看完重構姓建,致力于消除代碼中的每一處
if-else
和swtich
诞仓!殊不知對于缺乏反射的靜態(tài)語言,即使采用多態(tài)替換了條件分支速兔,但是最后在工廠方法里拼裝對象的時候還是會存在一個條件分支墅拭。這次總算是撥亂反正了! - 弱化了面向對象和類的分量涣狗,更多使用兼顧通用化的定義(例如使用
模塊
一詞替代類
)谍婉。關于這點,Martin Fowler在博客中曾如是說:
"在寫這本書第一版的時候镀钓,把類
視為是構建代碼機制的主要結構已經成為主流穗熬。然而,現(xiàn)在我們看到其它結構發(fā)揮了更大的作用丁溅。在我看來唤蔗,類仍然很有價值,但是重構已經比較少地以類為中心窟赏;我們要意識到措译,隨著代碼不斷被重構,類也是可以變化的饰序。" - 在某些局部的點上,優(yōu)先推薦了一些函數(shù)式的解決方案规哪,例如
把loops替換為管道
求豫。對于在新版中直接將
loops
定義為一種壞味道,個人認為是Martin新的一次矯枉過正。雖然管道和流在某些場景下比loops語義更清晰蝠嘉,但是不可否認在一些簡單的場景下loops更加符合一般程序員的思維習慣最疆。所以這得看具體的場景,一刀切有些不妥蚤告!不知道下一版中Martin會不會承認這類似第一版中的Switch Statement
一樣努酸,是一次故意的“矯枉過正”。
重構手法
正確的實施重構手法是安全高效的達成重構目標的保障杜恰。
很多人認為學習重構只要掌握背后的思想就足夠了获诈,其詳細繁瑣的操作手法并不重要。于是乎現(xiàn)實中很多人在實際操作重構的過程中章法全無心褐,一旦開始半天停不下來舔涎,代碼很多時候處于不可編譯或者測試不能通過的狀態(tài)。有時改的出錯了很難再使代碼回到正確狀態(tài)逗爹,只能推倒重來!
實際上重構是一項非常注重操作過程的技術亡嫌,能夠正確合理地使用重構手法,安全掘而、小步挟冠、高效地完成代碼修改,是評價重構能力的核心標準袍睡。否則Martin老先生也不值得花費多達四分之三的篇章對重構手法濃墨重彩了知染。
新版將重構手法從原來的68種調整到61種。由于具體的重構手法數(shù)量較多女蜈,這里就不一一對比了持舆,只介紹一些重要的變化點。
-
為了讓重構手法間銜接得更加順暢伪窖,新版中重新調整了重構手法的編排順序逸寓;
新版中將基本的常用重構放到了一章,起名叫做”第一組重構“覆山,然后將其余的手法按照”封裝“竹伸、”搬遷“、”數(shù)據(jù)“簇宽、”邏輯“勋篓、”API“以及”繼承關系“進行了分類和排序;
-
為了保持概念的內聚和一致魏割,對一些重構手法進行了重命名或者合并譬嚣;
例如將
引入Null對象(Introduce Null Object
改為引入特例(Introduce Special Case)
將函數(shù)改名(Rename Method)
、添加參數(shù)(Add Parameter)
和移除參數(shù)(Remove Parameter)
統(tǒng)一合并為改名函數(shù)聲明(Change Function Declaration)
钞它; -
通過調整原有的重構命名或者新增新的重構手法拜银,讓重要的重構同時存在正向和反向手法殊鞭,以便可以在實踐時更加靈活地決定重構的方向;
例如將原來的
內聯(lián)臨時變量(Inline Temp)
和引入解釋性變量(Introduce Explaining Variable)
改名為內聯(lián)變量(Inline Variable)
和提煉變量(Extract Variable)
尼桶;
將以函數(shù)取代參數(shù)(Replace Parameter with Methods)
修改為以查詢取代參數(shù)(Replace Parameter with Query)
操灿,并為其引入反向重構以參數(shù)取代查詢(Replace Query with Parameter)
; -
為了凸顯重構之間的關聯(lián)關系泵督,每種手法的介紹里面增加了
曾用名
趾盐、反向重構
和示意圖
-
曾用名
指出該重構在第一版中的原有命名; -
反向重構
指出該重構對應的的反向操作的手法名稱小腊,為重構之間建立起互逆關系救鲤; -
示意圖
是Martin為每個重構手法畫的一副小圖,形象化的展現(xiàn)每個重構的效果溢豆; - 另外蜒简,建議在閱讀時,除了關注每個重構手法的
做法
和范例
外漩仙,最好能把動機
部分也好好閱讀下搓茬。動機
部分介紹了”為什么要做“以及”什么時候不該做“這項重構,里面的很多思考都具有啟發(fā)意義队他。
-
-
考慮到編程范式的多元化卷仑,新版的描述中弱化了面向對象中的類的概念火架,同時引入了和函數(shù)式相關的一些重構杂拨;
在很多描述中,不再以
類
作為封裝的主要手段壹哺,取而代之以模塊
作為主要稱呼垢啼。
增加以管道取代循環(huán)(Replace Loop with Pipeline)
的重構窜锯; -
考慮到”組合優(yōu)于繼承“,調整刪除了一些和"重構到繼承"相關的重構手法芭析;
例如刪除了
提煉子類(Extract Subclass)
和以繼承取代委托(Replace Delegation with Inheritance)
等 -
由于JavaScript鴨子類型的語法特性锚扎,刪除了一些不再適用的重構手法;
例如刪除了
提煉接口(Extract Interface)
馁启、塑造模板函數(shù)(Form Template Method)
和封裝向下轉型(Encapsulate Downcast)
等 -
在某些重構的操作步驟里驾孔,利用了JavaScript的語法特性簡化重構的操作過程;
書中經常使用兩類JavaScript的語法特性:
- 函數(shù)式惯疙。正如Martin Fowler在博客中所說:”JavaScript將函數(shù)作為一等公民無疑是最正確的決策“翠勉。JavaScript允許將函數(shù)作為參數(shù)和返回值,允許在函數(shù)里面嵌套定義函數(shù)以及支持匿名函數(shù)和閉包霉颠。所以在提煉函數(shù)(Extract Function)
里面对碌,先將提煉出來的新函數(shù)定義在調用它的函數(shù)的內部,借助函數(shù)的閉包性可以減少函數(shù)間傳遞的參數(shù)數(shù)量蒿偎,等到真正需要復用該子函數(shù)的時候再將它挪出朽们;而以管道取代循環(huán)(Replace Loop with Pipeline)
重構克伊,則完全依賴于匿名函數(shù)和閉包性。
- 動態(tài)類型华坦。JavaScript支持靈活的對象模型,新版中很多重構手法會在操作過程中往對象里臨時加入新的屬性不从,以方便后面的重構過程惜姐。例如在函數(shù)組合成變換(Combine Functions into Transform)
和引入特例(Introduce Special Case)
中,都會看到引入了類似enrichXXX
名稱的函數(shù)對原有的對象進行動態(tài)擴充椿息;
對于重構手法這部分歹袁,Martin進行了精心的調整,修改和增加了很多描述和代碼示例寝优。上述只是其中一些比較明顯的變動条舔,更多細致的變化還請君仔細品讀原書。
最后要說的是乏矾,書中介紹了多達六十多種重構手法孟抗,普通人很難把所有的重構步驟都牢記于心。此書可以當做一本重構名錄钻心,在使用時再反復翻書查閱凄硼。相信經過不斷閱讀和實踐,慢慢會掌握重構手法背后的普遍原理和規(guī)律捷沸,逐漸做到”手中無劍摊沉,心中有劍“。我曾把所有的重構手法歸結為四類基本手法痒给,最后濃縮成了兩個核心操作说墨,并給出了在重構時合理編排操作步驟的方法規(guī)律以及指出如何利用錨點
來簡化重構操作。具體見《高效重構(二):掌握重構手法》苍柏。
重構示例
給初學重構的朋友一個建議尼斧,那就是在讀完全書后,最好能把第一章"重構序仙,第一個示例"中的例子再自行做一遍突颊。這個例子規(guī)模適中,代碼很具樣板性潘悼,非常適合用來演練重構律秃。
在第二版中,Martin考慮到很多新時代人類完全沒有在影片出租店租影片的體驗治唤,所以將例子改為”為戲劇演出的客戶打印賬單“的場景棒动。這個例子和第一版中”影片出租店打印小票“的結構基本類似。大家在練習的時候宾添,可以仔細品味以下方面:
- 由出現(xiàn)新的變化方向驅動重構的展開船惨;
- 將大的重構目標分解為一個個針對小的對代碼壞味道的重構過程柜裸;
- 在重構過程中執(zhí)行安全小步的代碼調整動作;
- 依賴自動化測試保護重構的安全性粱锐;
- 在重構的最后疙挺,代碼面向新的變化方向重新變得滿足開放封閉性;
最后再提一點:在書中怜浅,面對”賬單不同打印格式“的變化方向铐然,重構到最后產生了兩個函數(shù)renderPlainText
和renderHtml
,它們分別用來計算普通文本和HTML格式的賬單字符串恶座。對于像我這樣一個對重復代碼”吹毛求疵“的程序員搀暑,往往會進一步消除這兩個函數(shù)中出現(xiàn)的模式重復。這兩個函數(shù)有相同的打印算法跨琳,都是先輸出賬單標題自点,然后逐一輸出每個演出的具體細則,最后輸出賬單的匯總信息脉让,而且每一步中所依賴的數(shù)據(jù)是完全一致的桂敛。基于此我們可以做進一步的重構:將打印算法的重復和打印格式的不同進行分離侠鳄。完成這個重構后埠啃,代碼將會變成一個策略模式:由通用的打印算法組合不同的打印策略。現(xiàn)實中這個重構是否值得做伟恶,完全在于在不同的賬單格式下”打印算法相同“這個假設是否足夠穩(wěn)定碴开。
感興趣的同學可以在https://github.com/MagicBowen/refactoring/tree/master/code/js中找到上述重構的代碼示例.
寫在最后
本文簡單地介紹了第二版《重構》的一些變化,更多的精彩細節(jié)還是推薦大家閱讀原書博秫。這次中文版本還是由熊節(jié)先生主譯潦牛,所以書的翻譯質量依然很高。第二版由異步圖書出版挡育,書質還是蠻不錯的巴碗,又變成了硬皮封面,自然價格也有不小的上漲即寒。但是相信對于想要提高編碼能力的讀者來說橡淆,此書絕對是物超所值的。
不過母赵,還是有點小小的遺憾在這里吐槽一下逸爵。作為一個對代碼整潔度要求較高的人,書中偶爾出現(xiàn)的代碼縮進不對齊的問題會讓我覺得有些刺眼凹嘲。如果說有些縮進不對齊只關乎美感的話师倔,那么如下的不對齊就會讓人困惑于函數(shù)的定義位置了(沒法一眼看出函數(shù)playFor
是定義在頂層,還是在enrichPerformance
的內部)周蹭。
最后我們以一個小小的測驗結束本文吧趋艘。Martin先生為每個重構手法都繪制了一副示意圖疲恢,嘗試猜猜下面每幅圖對應的重構手法名稱,以及將對應的正反手法連接起來吧:)
作者:王博
Email:e.wangbo@gmail.com
轉載請注明作者信息瓷胧,謝謝显拳!