為什么要重構(gòu)
你可能正在面對一個(gè)遺留系統(tǒng)欧瘪,增加一個(gè)需求要改動好幾個(gè)文件腾供,定位 Bug 經(jīng)常要花掉一整天時(shí)間岖沛,修復(fù)一個(gè) Bug 可能又制造了 3 個(gè)新的 Bug。你也可能會為了軟件設(shè)計(jì)和同事爭得面紅耳赤廓握,討論如何應(yīng)對未來可能出現(xiàn)的需求變化搅窿。
為了開發(fā)一個(gè)新需求嘁酿,你打開一份源代碼,完全不知所云嘛男应,你吐槽著誰能寫出如此不堪入目的代碼闹司,于是決定查看版本記錄,把這個(gè)家伙找出來鄙視一下沐飘。然后你在提交歷史里看到了自己的名字... 恭喜你游桩,你進(jìn)步了。如果你是一個(gè)積極進(jìn)取的程序員耐朴,通常在幾個(gè)月甚至幾個(gè)星期之后就認(rèn)不出自己寫的代碼借卧。你總能發(fā)現(xiàn)更好的實(shí)現(xiàn)方式,讓代碼更加優(yōu)雅筛峭。
隨著增加新特性或需求變更铐刘,代碼會變得越來越難以維護(hù)。敏捷軟件開發(fā)的十二條原則中有一條是:我們始終擁抱需求變化影晓,哪怕是在軟件開發(fā)的后期镰吵。為了達(dá)到這種狀態(tài),我們就要在開發(fā)過程中持續(xù)地優(yōu)化代碼挂签。
而重構(gòu)這項(xiàng)技術(shù)疤祭,為我們提供了一種更可控的方式來優(yōu)化代碼。
重構(gòu)是什么
重構(gòu)饵婆,通常指的是「代碼重構(gòu)」勺馆,起源于 Smalltalk 圈子。
在日常工作中侨核,我們把重構(gòu)既作為名詞又作為動詞來使用谓传,作為名詞時(shí),它的意思是:
對軟件內(nèi)部結(jié)構(gòu)的一種調(diào)整芹关,目的是在不改變軟件可觀察行為的前提下,提高其可理解性紧卒,降低其修改成本侥衬。
所以我們會說,「這里需要做一個(gè)重構(gòu)」跑芳,「這個(gè)重構(gòu)有點(diǎn)問題」等轴总。
而在其它時(shí)候,我們也會說:「我們來重構(gòu)一下這段代碼吧」博个,「我正在重構(gòu)一個(gè)遺留系統(tǒng)」怀樟,這時(shí)就是把重構(gòu)當(dāng)做動詞在用,它的意思是:
使用一系列重構(gòu)手法盆佣,在不改變軟件可觀察行為的前提下往堡,調(diào)整其結(jié)構(gòu)械荷。
重構(gòu)本質(zhì)上是一種代碼整理技術(shù),這項(xiàng)技術(shù)使得代碼整理的效率更高虑灰,風(fēng)險(xiǎn)更小吨瞎。
如何做
接下來從幾個(gè)方面來說說如何做重構(gòu):
- 什么時(shí)候開始
- 什么時(shí)候停止
- 前提條件
- 重構(gòu)的過程
什么時(shí)候開始
重構(gòu)不應(yīng)該是一個(gè)單獨(dú)的環(huán)節(jié),應(yīng)該融入到開發(fā)軟件編寫代碼的過程中穆咐,就像使用版本控制系統(tǒng)提交代碼一樣颤诀,是一個(gè)必須做的動作。你不會跟項(xiàng)目經(jīng)理說对湃,我需要申請一段時(shí)間來提交代碼崖叫,所以也不用說服項(xiàng)目經(jīng)理給你時(shí)間重構(gòu)。你可以在開發(fā)新功能拍柒,修復(fù) Bug 的過程中就把重構(gòu)做了心傀,除了你的程序員同伴,沒有人知道你做了什么斤儿。而他們會認(rèn)為你做了一件了不起的事情剧包,因?yàn)槟阕尨a結(jié)構(gòu)更清晰了,以后添加新特性就會更容易往果,而 Bug 也無處藏身疆液。
如果你采用 TDD 的方式(測試驅(qū)動開發(fā)),那重構(gòu)已完全融入到了開發(fā)過程中陕贮。如果沒有采用 TDD堕油,通常有四個(gè)時(shí)機(jī)可以考慮要不要重構(gòu):
事不過三
如果有段代碼讓你修改起來很不舒服,前兩次還可以忍耐肮之,第三次就無需再忍了掉缺,果斷操起 IDE 重構(gòu)之。因?yàn)槌霈F(xiàn)了三次修改戈擒,說明有很大概率以后還會修改眶明,這是一筆劃算的投資。
添加新功能
有時(shí)候我們發(fā)現(xiàn)要添加一個(gè)新功能很難筐高,我們可以對代碼做一些重構(gòu)搜囱,讓添加新功能變得容易。
修復(fù)缺陷
在修 Bug 時(shí)柑土,我們大部分的時(shí)間會花在定位 Bug 上蜀肘,為什么這么難以找到呢?多半是因?yàn)榇a結(jié)構(gòu)不清晰稽屏,如果代碼在同一抽象層次上扮宠,每個(gè)方法都在 10 行以內(nèi),每個(gè)方法名和變量名都能清晰地表達(dá)意圖狐榔,Bug 就再無藏身之處坛增。所以获雕,通過重構(gòu)代碼,可以讓 Bug 自動浮現(xiàn)出來轿偎。
代碼評審
Code Review 已是一個(gè)廣泛采用的實(shí)踐典鸡,在 Code Review 時(shí),其他程序員會提出代碼修改的意見坏晦,記錄下來萝玷,等 Code Review 結(jié)束之后就可以開始重構(gòu)了。
什么時(shí)候停止
重構(gòu)到什么時(shí)候昆婿,我們就認(rèn)為可以停止了呢球碉?
有兩個(gè)標(biāo)準(zhǔn)可以參考,一個(gè)是「簡單設(shè)計(jì)」的四條原則:
- 通過所有測試
- 沒有重復(fù)
- 表達(dá)意圖
- 最少化程序元素(類仓蛆,接口睁冬,變量,方法等)
另一個(gè)是滿足《Clean Code》(整潔代碼)的要求看疙。
前提條件
現(xiàn)代 IDE豆拨,尤其是 JetBrains 公司的一系列產(chǎn)品,支持常用的重構(gòu)手法能庆,極大地降低了重構(gòu)的風(fēng)險(xiǎn)施禾。但為了保證不改變軟件的可觀察行為,還是需要完善的測試搁胆。我也做過一些沒有測試代碼保護(hù)的重構(gòu)弥搞,通常會加一個(gè)端到端測試以保證不破壞最重要的功能。實(shí)在很難編寫測試代碼渠旁,至少也要手工測試來保證重構(gòu)真的沒有改變軟件行為荤胁。
另一個(gè)重要前提是酌泰,使用版本控制系統(tǒng)摩疑,比如 Git吴侦。因?yàn)槲覀兊闹貥?gòu)并不一定總是令人滿意,也有可能出現(xiàn)錯(cuò)誤杂靶,導(dǎo)致軟件變得不可用承耿,所以最好是小步提交,以保證可以隨時(shí)放棄變更伪煤,回到上一次滿意的狀態(tài)。
重構(gòu)的過程
重構(gòu)的基本步驟是:
- 測試保護(hù)
- 識別味道
- 采用手法
- 運(yùn)行測試
- 提交代碼
測試保護(hù)
如果沒有測試代碼凛辣,就要先添加測試代碼抱既。如果有測試代碼,先運(yùn)行一下扁誓,保證在開始重構(gòu)之前防泵,測試是運(yùn)行通過的蚀之。還要認(rèn)真審查一下測試代碼,看是否有遺漏一些場景捷泞,有遺漏的話要補(bǔ)充遺漏的測試場景足删。
識別味道
怎么知道哪些代碼需要重構(gòu)呢?首先锁右,代碼是可以工作的失受,我們并不能說它有問題,但它又不像我們期望的那樣好咏瑟。受 Kent Beck 剛出生的女兒的使用的尿布的啟發(fā)拂到,Martin Fowler 和 Kent Beck 決定用「味道」這個(gè)詞來表示需要重構(gòu)的代碼。他們在《重構(gòu)》一書中列舉了 22 中常見的味道码泞,如果你看《Clean Code》的話兄旬,會發(fā)現(xiàn)還有更多。不過余寥,他們并沒有給出一個(gè)具體的標(biāo)準(zhǔn)领铐,而是需要我們的直覺來判斷。比如多大的類算「過大的類」宋舷?多少行代碼算「過長的方法」绪撵?這些需要自行判斷,而直覺的形成有兩種方法肥缔,一是隨著編碼經(jīng)驗(yàn)的增多自然形成莲兢,另一種更快的方式是大量閱讀優(yōu)秀的開源代碼,提高自己的代碼審美续膳。
《重構(gòu)》一書中的味道可以分為五類:
- 膨脹劑
- OO 使用不合理
- 難以修改
- 可有可無
- 耦合
書中都有詳細(xì)的解釋改艇,這里不再贅述。
發(fā)散式變化和散彈式修改是比較容易混淆的兩個(gè)味道坟岔。前者指一個(gè)類的職責(zé)過多谒兄,有很多因素會引起它的變化,具體的表現(xiàn)就是社付,不同的需求都會修改同一個(gè)文件承疲,導(dǎo)致經(jīng)常沖突,不能順利地并行開發(fā)鸥咖。后者指的是改一個(gè)需求要修改很多個(gè)文件燕鸽,說明沒有把強(qiáng)內(nèi)聚的代碼歸攏到一起。
大部分的注釋都是沒有必要的啼辣,注釋應(yīng)該描述「做了什么」和「為什么做」而不是「怎么做」啊研,方法體內(nèi)的注釋基本都可以通過抽取方法并指定一個(gè)有意義的名字來解決。很多為了應(yīng)對未來需求變化而寫的代碼基本永遠(yuǎn)不會被執(zhí)行。
你可能發(fā)現(xiàn)了党远,有些味道是比較容易識別的削解,比如重復(fù)代碼,注釋等沟娱。而有些就比較高級氛驮,比如特性依戀,中間人等济似,要識別高級味道矫废,需要理解面向?qū)ο蟮奶匦院驮O(shè)計(jì)原則。
采用手法
識別到味道之后碱屁,就要知道有什么對應(yīng)的手法可以消除這個(gè)味道磷脯,執(zhí)行完這個(gè)手法之后代碼會變成什么樣子。
在《重構(gòu)》一書中娩脾,列舉了 66 個(gè)常用手法赵誓,可以分為六大類:
- 重組函數(shù)
- 搬移特性
- 組織數(shù)據(jù)
- 簡化條件
- 簡化調(diào)用
- 處理概括
這些手法在書中都有詳細(xì)的講解,我就不在這里重復(fù)了柿赊。只整理出來俩功,給大家一個(gè)宏觀的印象:
運(yùn)行測試
在采用了手法修改代碼之后,就要執(zhí)行測試以確保真的沒有改變軟件的行為碰声」铗眩可能有時(shí)會發(fā)現(xiàn),做了重構(gòu)之后測試會失敗胰挑,但實(shí)現(xiàn)并沒有問題蔓罚,我們需要修改測試代碼讓它成功。這就說明測試寫的不合理瞻颂,給重構(gòu)帶來了負(fù)擔(dān)豺谈,所以我們測試的粒度要把握好,太細(xì)的粒度就會增加維護(hù)成本贡这。比如茬末,有些人會給每個(gè)私有方法都寫單元測試,那有可能采用「內(nèi)聯(lián)函數(shù)」這個(gè)手法之后這個(gè)方法就不存在了盖矫,就需要修改測試丽惭。這里說起來話就長了,以后再寫一篇如何寫有效的測試的文章吧辈双。重點(diǎn)是重構(gòu)之后责掏,一定要執(zhí)行測試,不管是手工測試或自動化測試湃望。
提交代碼
最后换衬,如果你采用了一個(gè)比較復(fù)雜的手法局义,或者即將采用一個(gè)復(fù)雜的手法,最好先提交一下代碼冗疮,以保證出現(xiàn)意外后能快速回滾,避免浪費(fèi)時(shí)間檩帐。
重構(gòu)要采取「小步快跑」的原則术幔,盡量采用安全的手法,讓測試一直處于通過的狀態(tài)湃密。
從低級的壞味道開始诅挑,消除低級味道之后,高級味道才會浮現(xiàn)出來泛源。
進(jìn)階
重構(gòu)與設(shè)計(jì)的關(guān)系
在沒有重構(gòu)這個(gè)技術(shù)之前拔妥,廣泛采用的是 Big Front Design,在開始編碼之前要進(jìn)行非常詳細(xì)的設(shè)計(jì)达箍,考慮應(yīng)對未來出現(xiàn)的各種變化没龙。而有了重構(gòu)技術(shù)之后,前期設(shè)計(jì)的壓力就小了缎玫,畢竟可以隨時(shí)通過重構(gòu)來改善設(shè)計(jì)硬纤,應(yīng)對變化。所以你大可不必一上來就應(yīng)用《設(shè)計(jì)模式》把代碼搞復(fù)雜赃磨,先用簡單的實(shí)現(xiàn)滿足當(dāng)前需求即可筝家。等變化真正來臨時(shí),再通過重構(gòu)技術(shù)調(diào)整設(shè)計(jì)邻辉,模式給我們提供了一個(gè)方向溪王,但并不是最終目標(biāo)。還記得簡單設(shè)計(jì)的四條原則嗎值骇?通過測試莹菱,沒有重復(fù),表達(dá)意圖雷客,最少元素芒珠。除了這四條原則,還有 SOLID搅裙,DRY皱卓,KISS 等設(shè)計(jì)原則。只要最終的代碼符合好的原則部逮,干凈整潔沒有壞味道娜汁,管它符不符合某個(gè)模式呢?兄朋!
大型遺留系統(tǒng)的重構(gòu)
對于代碼上百萬掐禁,千萬行的遺留系統(tǒng),怎么重構(gòu)呢?滿地都是壞味道傅事,一點(diǎn)點(diǎn)去重構(gòu)缕允,什么時(shí)候是個(gè)頭?
這時(shí)蹭越,選擇哪些代碼來重構(gòu)就非常重要障本,影響到投資回報(bào)。如果對代碼進(jìn)行分類响鹃,將會得出幾種類型:
- 不會被執(zhí)行的爛代碼
- 運(yùn)行穩(wěn)定驾霜,基本不會改動的爛代碼
- 經(jīng)常發(fā)現(xiàn) Bug 的爛代碼
- 經(jīng)常需要變更的爛代碼
不會被執(zhí)行的代碼,直接刪除就好了买置。運(yùn)行穩(wěn)定的又不需要改動的粪糙,動它反而可能引入風(fēng)險(xiǎn),當(dāng)然忿项,在時(shí)間充裕的情況下蓉冈,還是可以重構(gòu)的。真正有價(jià)值倦卖,值得重構(gòu)的洒擦,投入產(chǎn)出比最高的,是經(jīng)常出問題和經(jīng)常會有需求變更的爛代碼怕膛。優(yōu)化了這部分代碼熟嫩,可以減少 Bug 和進(jìn)行需求變更的時(shí)間。
好了褐捻。關(guān)于重構(gòu)我想分享的就是這些掸茅,我們來回顧一下:
為什么要重構(gòu)?
為了讓軟件始終可以維護(hù)柠逞,保證開發(fā)效率昧狮。
什么是重構(gòu)?
一種以可控的方式整理代碼的技術(shù)板壮,在不改變軟件可觀察行為的前提下改善其內(nèi)部結(jié)構(gòu)逗鸣。
什么時(shí)候開始?
事不過三绰精,添加功能撒璧,修復(fù) Bug,代碼評審時(shí)笨使。
什么時(shí)候停止卿樱?
重構(gòu)到符合簡單設(shè)計(jì)四條原則的 Clean Code。
前提條件
測試保護(hù)硫椰,版本控制繁调。
重構(gòu)的過程
運(yùn)行測試萨蚕,識別味道(常見的 22 種),采用手法(66 個(gè))蹄胰,運(yùn)行測試岳遥,提交代碼。
重構(gòu)與設(shè)計(jì)的關(guān)系
有了重構(gòu)技術(shù)裕寨,我們不用在前期做非常詳細(xì)的設(shè)計(jì)寒随,做適當(dāng)?shù)脑O(shè)計(jì),然后通過重構(gòu)讓設(shè)計(jì)浮現(xiàn)出來帮坚。不用在乎軟件是否符合模式,只要符合原則即可互艾。
大型遺留系統(tǒng)的重構(gòu)
在經(jīng)常需要修改的爛代碼上做重構(gòu)才有最大收益试和。
最后推薦一些學(xué)習(xí)資源:
- 《代碼整潔之道》
- 《編寫可讀代碼的藝術(shù)》
- 《重構(gòu)》
- 《設(shè)計(jì)模式 - 可復(fù)用面向?qū)ο筌浖幕A(chǔ)》
- 《重構(gòu)與模式》
- Transformation Priority Premise
- 用 IntelliJ IDEA 重構(gòu)
- 重構(gòu)十六字心法
- 練習(xí)重構(gòu)的 Kata
本文最初發(fā)布于 GitChat,歡迎閱讀答疑實(shí)錄纫普。