修改軟體的藝術:如何重構遺留程式碼
重構是指在不改變外部行為的前提下對程式碼的內部結構進行重組或重新包裝。
想象一下,如果你是若干年前的我,正在對經理說你要讓整個團隊花上兩週(一個完整的迭代週期)來重構程式碼。經理問:“好的。你會給我什麼樣的新功能呢?”
我說:“等等。我是說重構。重構修改內部結構而不改變外部行為。不會有任何新功能。”
他看著我問道:“那你為什麼要重構?”
我應該如何回答?
軟體開發者時常遇到這樣的情況。有時候不知如何作答,是因為我們和管理層存在溝通障礙。我們使用的是開發者的語言。
我不能告訴經理重構程式碼是為了好玩,是因為它讓我感覺良好,或者因為我想要學習Clojure或者其他新技術……這些對管理者來說都是不可接受的答案。我必須強調重構對於公司的重要意義,而且它確實意義重大。
開發者知道這些,但需要用恰當的詞彙也就是商務用語來表達,其實就是收益和風險。
我們如何在降低風險的同時提高收益?
軟體本身的特點決定了其高風險和多變性。重構能降低以下四個方面的成本:
-
日後對程式碼的理解
-
新增單元測試
-
容納新功能
-
日後的重構
很顯然,如果需要新增新功能或修復bug,就應該重構程式碼,這很容易理解。如果你以後不再碰程式碼,也許就不需要重構了。
重構是學習新系統運作機理的有效方式。通過對見名知意的方法進行封裝或重新命名來學習程式碼。通過重構,我們可以補充之前缺失的實體,改善之前編寫的糟糕程式碼。
我們都希望看到進度並如期交付,所以有時會做出妥協。重構程式碼能清理之前造成的障礙,為的是最終能夠有所成果。
1 投資還是借貸
我在2009年4月的部落格中首次講述了以下故事。1
1Bernstein, David Scott. To Be Agile (blog). “Is Your Legacy Code Rotting?” http://tobeagile.com/2009/04/27/is-your-legacy-code-rotting
有些經營者認為開發軟體是種一蹴而就的活動。如果軟體在編寫完成後不會變更的話確實如此,但我們身處的世界在變化,不變的軟體很快就過時了。
程式碼的衰變是真實存在的,即使一開始編寫良好的軟體也常常難以預計將來會面臨的變更。這實際上是好事!不需要變更的軟體通常是沒人使用的軟體。我們希望自己構建的軟體為人所用,為了軟體能持續給人帶來價值,它需要容易修改。
我們可以用紙板建造一間漂亮的房子,在晴朗的夏日它能支撐得住,但第一場暴雨就能摧毀它。建築工人有一系列嚴格的標準和實踐用來保證建築的穩固。軟體開發者也應該這樣做。
我在東海岸的一位客戶是全球最頂尖的財務公司之一,他們飽受大量遺留程式碼的摧殘。大部分的遺留程式碼都是由合同工開發的,有些雖然由他們中的頂尖開發者開發,但是在第一版本完成後,這些開發者立刻加入了其他專案。一些初級開發者被指派來維護這些系統,有些人並不理解最初的設計,所以對系統強加修改。最後弄得一團糟。
在一次他們的高階經理及高階開發者參加的會議上,我說:“我這樣說對不對?你們之所以成為頂尖的財務公司,是因為你們找到一流的基金經理管理你們的基金,權衡最佳投資,然後凍結那些資產,將這些基金經理撤出去做其他專案。”
他們說我前半句說得沒錯,但是基金經理一直管理著基金,持續對資產做出調整,因為市場瞬息萬變。
“哦,”我說,“所以你們僱用一流的軟體開發者,讓他們進行設計,開發系統,在他們完成後就換到其他專案中。”
“你的意思是我們的軟體也是一種關鍵資產,和其他資產一樣需要維護?”一個經理問道。
答對了。
你會買一輛9萬美元的賓士車,然後因為嫌花費太多而不將其送去保養嗎?不會。無論車有多昂貴,製造工藝有多精良,都是需要維護的。沒有人會在蓋房子的時候想著永遠也不會更換地毯,添置新廚房器具或重新粉刷。反之,也沒有人會在開車上路三天後就換變速器,理由是早晚也得換?如果有天發現倒車檔失靈,你會在這種無法倒車的情況下開多久再去檢修變速器呢?
有些事情最好放到最後處理,有些則不能推後。知道這兩者之間的差別絕對重要。
對於那些會不斷累積的技術債,儘快償清幾乎總是(肯定會有例外)正確的選擇。如果任由技術債在系統中堆積,而開發者又在系統中工作,那麼絕對會發生衝突。開發者會碰到那些技術債並且一次次付出代價。他們沒法倒車,所以必須調整他們的行為(駕駛習慣),繞彎路到達目的地,為的是不使用倒車。一個問題會導致更多的問題。越早處理技術債,花費的成本就越低,就像信用卡欠款一樣。
2 變成“鐵公雞”
技術債和財務債一樣:利息會把你拖垮。
我曾經和財務信貸公司合作過,他們有一個不願意示人的詞語用來形容我這類人。我是那種總是在收到賬單時就全額還款的人,從來不讓我的賬上產生利息。信用卡公司稱我這樣的人是“鐵公雞”,因為無法從我們這樣的人身上掙到錢而討厭我們。他們喜歡那些讓債務堆積每次只償還最小還款額的人。我認識的一個人欠了一家信用卡公司1.7萬美元的債。如果他每個月只還最小還款額的話,需要花93年共計18.4萬美元才能還清。
和財務債一樣,無視問題並不能讓問題消失。我希望你成為技術債的“鐵公雞”。
有時候,我們必須讓技術債多存活一陣子,要不就是現在不是修復的最佳時機,要不就是我們不知道如何下手,或者單純的沒有足夠時間而已。我們會經常無可奈何地發現自己處於這番境地。但是,先支付幾個月的最小還款額來度過難關,然後再連本帶利一起還清,和裝作相安無事直到二十二世紀,這有很大差別。
我們並非試圖建立完美的程式碼。我始終在強調這一點。沒人可以做到完美無缺,軟體開發者也不是在追求完美無缺。我們必須時刻清楚地權衡利弊。我是否時不時在程式碼中引入了問題?是的,無可避免。如果不這樣,我就會丟掉飯碗。
3 當代碼需要修改時
即使是寫得最糟糕的遺留程式碼,如果我們不碰它的話也能持續產生價值——只要不進行修改。
這種判斷應該分不同情況討論。任務關鍵型軟體的需求和視訊遊戲完全不同。軟體之於實物機械的一個好處就是,資訊不會磨損。但是,遺留程式碼需要修改和擴充套件的時候會怎樣呢?
如果軟體真的被使用了,人們就會發現更好的使用方式,然後提出修改需求。如果想讓使用者從你建立的軟體中獲得更多價值,就需要找到安全的方式來改進程式碼,以支援變化的需求。
既然我們已經知道優質程式碼的一些特性,就可以用重構這個工具來安全地、漸進地將程式碼變得更容易維護和擴充套件。對設計糟糕的程式碼進行安全地重構,讓我們可以注入模擬物件來使軟體可測試,這樣就可以在程式碼中新增單元測試。單元測試這張防護網可以支援我們為了安全地新增新功能而進行更復雜的重構。
這樣清理遺留程式碼可以在遺留程式碼之上工作而又不用擔心引發新bug:進行漸進式修改,新增測試,然後新增新功能。如果有良好的單元測試來覆蓋程式碼,就可以在綠條之上進行新的開發和重構了。這是更安全也更廉價的修改軟體的方式。
在軟體行業中,有許多的程式碼——遺留程式碼——並未按照我們預期的那樣運作良好,完全沒法維護,更不用說擴充套件了。但是我們能做些什麼?又應該做些什麼呢?
多數的情況是,什麼也做不了。
在軟體行業,我們不應該將遺留程式碼視為定時炸彈,而是地雷。如果程式碼正常工作,不需要修改或升級,就不要動它。這適用於絕大多數遺留程式碼。正如有句諺語所說:“東西沒壞,就別去修。”
如果我們亂碰那些遺留程式碼,一定會出問題。如果程式碼如預期的那樣執行,就這樣使用。這對於大多數現存的軟體來說都是正確的。一般來說,只重構需要修改的程式碼。
如果你想修復程式碼中的bug,或者新增新功能,或者修改現有功能,對已有程式碼進行修改就非常有必要。修改程式碼的風險和成本都很高,所以要謹慎行事。但是,如果真的需要修改程式碼,就應該使用正確的方法,好讓修改變得安全。事實上,這些修改程式碼的技巧和我們之前討論的編寫優質程式碼的技巧是一樣的。
我們可以用重構新程式碼的方式重構已有程式碼。
3.1 對已有程式碼新增測試
測試先行能讓程式碼更容易測試,後期新增測試比測試先行更有挑戰,卻也能從整體上提高程式碼的可維護性,降低修改程式碼的成本。
變更請求是好事,意味著有人關心並希望程式碼得到改善。
得到變更請求之後,我們希望能夠做出響應,在已有的軟體中提供新功能,讓客戶能夠在使用過程中得到更多價值。當然,還有許許多多的程式碼已經無人問津。那些程式碼可以安靜地被淘汰,但是,那些正在使用著的位元——客戶所倚仗的很可能會變化的那些位元——是我們重構的目標。
3.2 通過重構糟糕程式碼來培養良好習慣
重構是一個未被所有開發者都理解的技巧,重構也是培養良好開發習慣、展示構建可維護程式碼方法的工具。這些技巧自始至終是軟體工程師應必備的技巧。
重構遺留程式碼聽上去很無聊,但事實上充滿驚奇和挑戰。不斷練習會讓人得心應手。精通重構之後,有趣的事情就會發生:我們不再會寫出糟糕的程式碼或遵循有缺陷的開發實踐,而開始前構(參見《前構》[Pug05]),也就是說可以一開始就寫出優質的程式碼。學習如何避免軟體開發中的錯誤,如何正確進行開發,重構是我所知的最快速的方式之一。
3.3 推遲那些不可避免的
身為軟體開發者,我們的目標是通過構建有價值的軟體來創造價值。這意味著我們開發出來的軟體需要能立刻產生價值,並在以後的日子裡持續產生價值。
為了讓軟體在將來持續產生價值,必須降低軟體所有者的開銷,這樣對軟體進行改進和擴展才會有收益。讓軟體健壯且可維護是我們的首要目標,這樣會降低軟體所有者的開銷。
但無論早晚,軟體都會被淘汰。有些我編寫的軟體存活的時間讓我大吃一驚。我在孩童時代編寫的並不引以為傲的程式碼卻以某種形式存活至今。
軟體有時會夭折,有時也會比我們預期的存活得更長久,我們在編寫軟體的時候完全沒法準確預估將來到底會怎樣。但是我們都希望自己的軟體能夠持續產生價值。應該盡我們所能,提高對投資的回報,降低軟體所有者的開銷。
4 重構技巧
在我們清理程式碼讓其更容易測試的時候,也讓它變得更容易擴充套件且降低日後修改的開銷。以下是一些重構程式碼的技巧。
一般來說,重構遺留程式碼從功能層次開始,因此可以根據一些可觀測的行為編寫圖釘測試。
4.1 圖釘測試
圖釘測試是非常粗粒度的測試,它測試的可能是成百上千行程式碼的單一行為。雖然最終期望的是許多更細粒度的測試,但是,通過圖釘測試來覆蓋整體行為會讓我們有一個落腳點。每次修改程式碼後,都可以回到圖釘測試來驗證最終點對點的行為是否依然正確。
由於圖釘測試粒度很粗,因此必須頻繁執行來驗證修改是否對程式碼造成影響。這可以給一些相對安全的重構行為提供防護網,讓程式碼中有更多的間隙可以進行依賴注入。這將有效解耦物件和它們所使用的服務,讓我們可以用模擬物件替代服務,以便獨立測試指定程式碼。這讓更小單元的行為變得可測試,可以新增更細粒度的單元測試,用來支援更復雜的重構。
這就是重構遺留程式碼的方式,一點一點,進行小規模增量優化。遺留程式碼的產生是因為一直以來開發者所做的小修改降低了程式碼質量。修復的方式也是用小規模的程式碼修改來增進程式碼質量,然後逐漸減輕遺留程式碼的負擔。
4.2 依賴注入
我們在之前討論過分離物件的建立和使用的意義。這是讓程式碼變得可測試的重要環節,同樣也讓程式碼變得可以獨立部署。分離了物件的構建和使用,我們就可以在不引入耦合的情況下注入所需的依賴。這是面向物件程式設計的基本技巧之一。
各種框架都會使用這種技術,像Pivotal Software的開源Spring和Red Hat的Hibernate,都被稱為依賴注入框架2。原理很簡單:我們不直接建立要使用的物件,而是讓框架替我們將物件注入到程式碼中。3,4
2準確地說,Spring是依賴注入框架,而Hibernate是使用了依賴注入的ORM框架。——譯者注
3Spring. http://spring.io
4Hibernate. http://hibernate.org
用依賴注入取代建立可以解耦物件和它們所使用的服務,這會讓軟體更容易測試和擴充套件。如果不注入真實的依賴而是注入模擬依賴,就很容易測試程式碼。依賴注入也會使程式碼更容易維護,它有助於將業務決策集中化,簡化物件使用方式。通常,為了理解一個新系統,首先看的就是物件例項化的地方。我們檢視工廠物件,依賴注入框架,或者其他進行物件例項化的地方。這會告訴我們很多關於系統的資訊,讓我們知道如何改進它。
4.3 系統扼殺
如果需要在不影響系統的前提下替換一個元件,通常使用Martin Fowler在2004年最早提出的系統扼殺5。先用一個自己的服務將原來的服務包裹起來,然後一點點替換原來的服務,直到原來的服務最終被扼殺。
5Fowler, Martin. Martin Fowler (blog).“Strangler Application.”June 2004. http://www.martinfowler.com/bliki/StranglerApplication.html
為新的服務建立一個新的介面來取代老的服務。然後客戶端程式碼使用新的介面,即使新的介面僅僅是指向老的服務。這樣可以阻止老的服務繼續擴散使用,讓新的客戶端使用新的介面,新的介面背後的程式碼最終會被替換為新的整潔的程式碼。
這樣就可以不慌不忙地重構已有的系統,直到老的介面僅僅是薄薄一層外殼,它只是對重構好的新程式碼進行呼叫而已。可以選擇繼續支援遺留的客戶端,或者讓它們也進行重構,用全新的介面徹底淘汰遺留系統。系統扼殺是非常有效的重構遺留程式碼的方法,它可以在重構的同時保持系統持續可用。
4.4 抽象分支
這裡要介紹的最後一個技術也是Martin Fowler提出的,它叫抽象分支6。先不管名字,這是一個版本管理技巧,幫助我們消除分支。原理是針對想要修改的程式碼提取出一個介面,然後編寫新的實現,但是老的實現依然參與構建,在構建期間用功能開關隱藏正在開發的功能不讓使用者感知到。
6Fowler, Martin. Martin Fowler (blog).“Branch by Abstraction.”January 2014. http://martinfowler.com/bliki/BranchByAbstraction.html
一切就緒之後,可以開啟功能開關用新的介面替代老的介面。這是簡單且直觀的方法,卻消除了軟體的版本分支依賴。版本分支依賴對於很多軟體開發團隊來說是個大問題。正如之前所說,一旦開始用功能分支,我們就不再是進行迭代開發,而是淪為了瀑布式開發。
有時系統耦合之緊密,無法在不影響其他地方的情況下打破依賴。這時Ola Ellnestam和Daniel Brolund的《天皇法則》(The Mikado Method)[BE12]能幫助我們解決這種糾纏不清的依賴。
5 以支援修改為目的重構
當然,還有許多其他應對遺留程式碼的技巧。基本原理都是:清理程式碼,讓程式碼容易維護、容易理解,然後新增測試以便安全地進行修改。最後,必須在有單元測試的保障之後,對程式碼進行大規模重構。
我們把重構當作一門學問,而且還有很廣泛的研究空間。我們需要繼續研究這種規範化的程式碼修改方式。我更願意有一套安全且可複用的修改程式碼方法論來分享給其他開發者,而不是憑著直覺修改程式碼(這種直覺無法直接告訴其他人),儘管有時候這樣做效率更高。
重構軟體的目的就是可以容易地根據客戶的希望修改軟體。這無法通過閱讀客戶的思想或預知未來來達成,而是遵循著一些健壯性的原則和實踐,讓程式碼在需要的時候可以被修改。這需要有一套單元測試、準確的領域建模、合理的抽象、CLEAN的程式碼以及其他優秀的技術實踐。當我們做到這些的時候,會讓修改程式碼變得無痛,可以隨時修改程式碼,更好地迎合客戶的需求。
6 以開閉原則為目的重構
這是改變我一生的幾個字,它幫助我擺脫了遺留程式碼的窠臼。重構是在不改變外部行為的前提下調整設計。開閉原則是指軟體實體應該“對擴充套件開放而對修改關閉”。換句話說,力求在新增新功能的時候做到新增新程式碼並將現有程式碼的修改最小化。避免修改現有程式碼是因為很可能會引發新的bug。
以開閉為目標重構是安全高效新增新功能的方式。讓每一個改動都分為兩步。第一步重構想要擴充套件的程式碼,讓它可以容納新功能。這並非新增功能,而是在原有的軟體中通過新增抽象或定義介面之類的方式來給新功能建立空間。在有單元測試的前提下重構程式碼來容納新功能,這樣做是安全的、沒有阻礙的,如果你犯了錯單元測試立馬就會告訴你。這是安全且代價小的修改程式碼的方式。
當代碼重構完成可以容納新功能的時候,第一步重構階段就完成了。接下來的一步是增強階段。先編寫失敗測試描述要實現的新功能,然後新增功能讓測試通過,以開放的增量方式開發。我們只新增程式碼,因為之前的重構階段已經完成了修改程式碼。我們可以在不修改過多程式碼的前提下新增程式碼,這樣更安全。最後,重構新新增的程式碼,讓它容易理解和維護。
如果嘗試一次將所有的事情都做完,正如很多開發者做的那樣,那很容易迷失其中然後犯錯誤,結果為之付出巨大代價。但是,在有單元測試覆蓋的前提下分階段做,會讓修改軟體變得更簡單、風險更小。
我總是將修改程式碼分為兩個步驟,這樣做是因為我通過TDD來構建功能。需要在現有系統中新增功能的時候我也這樣做。前面討論的許多實踐不僅對編寫新程式碼適用,對遺留程式碼也同樣適用。一旦理解了優質程式碼是什麼樣的,就更容易認識到重構遺留程式碼的時候應該追求什麼。
7 以提高可修改性為目的重構
程式碼的可修改性並不是意外產生的。必須有意在新程式碼中建立,或者小心地在重構遺留程式碼過程中通過遵循優秀的開發原則和實踐引入。
這對於任何專業來說都是一樣的。醫生不可能揮揮手就神奇地治癒了病人。儘管有時候患者會自愈,但軟體無法自我編寫。你必須讓計算機執行你的命令。
程式碼對可修改性的支援意味著找到合理的抽象且程式碼封裝良好。歸根結底,可修改性來自於理解所建模事物並將這些理解連帶著各種特性都灌輸到模型中去,讓模型準確、一致。
這些實踐不會替我們做設計。TDD對設計有幫助,但我們不能停止思考讓TDD替我們編碼。TDD是一個工具,可以幫助我們理解構建易修改程式碼的流程,這個流程尤為重要。
科學與藝術的一個區別是,科學常常有一定的流程(一個程序或者一個程式)。這就像根據菜譜做飯,我們可以從同樣的菜譜開始,做一些不同的修改,然後做出風味各異的同一道菜餚。我們始終遵循同樣的實踐:如何煎炸,如何切菜然後烹飪口感更好,如何避免做出半生半熟的雞肉,等等。
流程對軟體開發來說至關重要。雖然我們面對的每個問題都各不相同,需要不同的方法來解決,但是解決軟體問題的基本通用流程還是有的,有些甚至是反直覺的,就如同測試先行和最後進行設計。
8 第二次做好
TDD用真實的用例來定義行為。用真實的用例來驅動開發比抽象的思考要容易。它有助於構建更穩定的介面,當我們有兩三個用例之後,將程式碼通用化會比只有一個用例來得簡單得多。我喜歡這樣的說法:
第二次做好。
在聽到我這樣說之後,開發者有時候會用奇怪的眼神看我,好像我瘋了一樣。我們一直被教導著凡事要一次做好。
但是,當我們一次做好(或者說認為自己一次做好)的時候,我們給自己添加了很多額外的工作。只有一個用例很難進行通用化設計。在只有一個用例的時候進行具體化的編寫,在有了兩三個用例之後進行通用化就相對容易了。我們可以觀察各個用例之間的異同,然後進行總結,找到合理的抽象。
在天文導航中,三角定位法是一個非常常用的手段。通過多個水平線上的點或者多顆星星得到的定位,比只通過一個點得到的定位要準確得多。編寫程式碼也是同樣的道理。
如果有一個非常複雜的演算法,不確定如何才能完美解決,先建立幾個用例。通常在兩個(最多三個)用例的輔助下,就能推匯出真正的演算法了。這比只通過一個用例來猜想要容易得多。
有了兩個用例,就可以開始對每一步進行總結,得出正確的抽象。所以,如果只有一個用例,就直接把它編寫出來,當然,是使用測試先行開發。這讓我們可以用真實的單元測試來驅動行為的開發,而且在得到第二個用例要重構程式碼的時候也提供了安全保障。
軟體是軟的,利用這個特性我們可以更輕鬆地構建出更優質、更靈活的程式碼。這是重中之重。
相反,試圖一蹴而就會有很大的壓力。對於所有人來說都一樣。知道可以回過頭去修改、隨時清理(可以在任何時候重構),會讓我們很自由。
研究重構是學習如何從一種設計轉變到另一種設計的最佳方式。這種轉變大都很容易,理解了如何轉變設計,也就意味著可以不用試圖在一開始就找到最佳設計,我們可以隨著重構來不斷改進設計。
對於習慣了確定性的我們,這樣的策略乍看之下有些奇怪。它認定我們是處在一個萬事萬物都在變化的難以預計的世界之中。在習慣了這種開發方式之後,可以更快速地構建更優質的程式碼,而不需要提前將所有的需求都準備完畢才開始構建。這讓人很自由,也是因為如此,我發現它能讓開發者產生共鳴。
9 讓我們付諸實踐
以下是把這些想法付諸實踐的方式。
9.1 助你正確重構程式碼的7個策略
重構給予開發者改進設計的機會,也讓管理層以廉價且低風險的方式在已有系統中新增功能。以下是幫助你正確重構程式碼的7個策略。
從已有系統中學習
重構是一種學習程式碼的方式,也是將學到的東西融入程式碼的方式。比如,將命名不合理的方法用更合理的名字取代或包裹起來,可以提高程式碼的可讀性。同時,我們也學習到了系統是如何工作的,並且將這些理解通過提供更合理的命名融入到了新程式碼中。
循序漸進
低風險重構是重構方法中的子集,可以相對簡單地執行。大多數可以通過開發工具進行自動化,比如在一個工程內重新命名、提取、移動方法和類。我在編碼的時候經常使用這些重構方法,讓程式碼時刻反映我對所構建系統的最新理解。
在遺留程式碼中新增測試
所有的重構都會降低四件事情的開銷:日後理解程式碼、新增測試、新增新功能、更多的重構。在重構的過程中會發現改進設計、新增單元測試的機會。新增更高質量的測試之後,會更有信心進行更激進的重構,進而給編寫更多高質量測試創造機會,如此往復。
始終進行重構
重構是需要自始至終進行的。程式設計通常都是一個發現過程。也許在探索的過程中並不知道最好的方法,所以在有了更好的理解之後才有更多的機會去改進程式碼、更新命名等。這是保持程式碼容易使用的關鍵。如果實踐TDD,就會知道在TDD過程中,重構是在編寫出可用實現之後立刻進行的。這樣可以提高程式碼的健壯性,減少維護成本,提高可擴充套件性。
有更好的理解後對一個實現進行重新設計
即使持續重構,也依然會在開發過程中產生技術債。當得到會影響設計的新資訊或者需要實現一個當前設計不支援的功能時,可能是時候做一次大規模的重構了。也許需要大範圍的重新設計和實現,讓以後更容易新增新功能。
繼續其他工作前進行清理
一旦完成了某些工作,應在繼續其他工作前重構現有程式碼,讓其更加健壯。在知道了每個方法的職責之後,保證它們有合理的名稱來表達其意圖。保證程式碼容易閱讀、分佈合理。將大方法拆分為小方法,必要的時候提取出額外的類。
重構以避免誤入歧途
現如今多數生產環境中的軟體都積累了大量的技術債,迫切需要重構。這看上去像是一個可怕的任務,也確實可能會如此,但是重構程式碼也可以非常有趣。我發現自己在重構程式碼時收穫頗豐,在花費大量時間清理他人(或自己)的錯誤後,在編寫新程式碼的時候能避免類似的錯誤。隨著重構越來越多,我也隨之成為更優秀的開發者。
重構是為了降低風險減少浪費。高效的開發團隊可能花費一半的時間在重構程式碼上,同時也優化了他們的設計,提高了系統的健壯性,時間花得很值。因為程式碼被閱讀的次數是編寫次數的十倍以上,利用重構來清理程式碼會很快得到收益。
9.2 決定何時進行重構的7個策略
鑑於整個行業中需要重構的程式碼遠遠多於我們的承受能力,我們需要決定對哪些程式碼進行重構。如果生產環境上的軟體正常工作不需要擴充套件,則無需重構程式碼。重構程式碼有風險和成本,所以我們希望最後收益能夠抵得上開銷。以下是決定何時進行重構的7個策略。
當關鍵程式碼維護不善的時候
多數軟體的狀況無法進行安全的重構。如果程式碼處在生產環境,對它進行修改,即使是看上去很小的改動,也會造成未知的破壞。因此,別去碰觸遺留程式碼總是明智的。但是當關鍵程式碼難以理解變成累贅時,就是時候進行清理了。這種場景下,新增測試來支援更復雜的重構非常有效。
當唯一理解程式碼的人沒空的時候
我們編寫的軟體應該可以讓團隊的其他成員容易理解和維護,但是,有時現有的程式碼只有那些特定的“專家”才能維護。這對公司來說不是一件好事。如果程式碼需要維護更新,讓關鍵人員在繼續其他工作前花時間清理程式碼,這可以避免後期花費更大的成本進行清理。
當有資訊可以揭示更好的設計的時候
需求,以及我們對於需求的理解,都是一直在變化的。當有了更好的設計方案,而且收益比成本要高的時候,重構就是個好主意。這是一個持續進行的過程,用來保證軟體整潔且與時俱進。通過一系列的重構來改進設計,是非常有效的保持軟體可維護的方法。
當修復bug的時候
有些bug僅僅是拼寫錯誤而已,而有些則代表了設計上的缺陷。很多時候,程式碼的bug體現了開發流程上的缺陷,或者至少是系統中一個缺失的測試。也許是因為難以編寫測試所以缺失,這樣的話,我們可以重構程式碼,讓編寫測試變得容易,然後補全測試。接著再修復bug,測試通過之後則一切正常。
當需要新增新功能的時候
向不相容新功能的系統中新增新功能的最廉價、最安全的方式就是,先重構程式碼讓系統可以相容新功能,重構完畢之後再新增新功能。我們不會希望自己同一時間對多處程式碼進行修改。為新增新功能而重構程式碼,通常需要新增新的抽象和介面,讓新功能更容易插入到現有系統中。重構之後,向程式碼中新增新功能就應該輕而易舉了。
當需要為遺留程式碼寫文件的時候
有些程式碼很難理解,在編寫文件前通過簡單的重構和清理就能有很大幫助。編寫文件的目的就是為提高系統的保障性,這也是重構的目的之一。
當重構比重寫容易的時候
將生產環境上的系統直接拋棄徹底重寫,幾乎從來都不是個好主意。重寫一個應用通常都會比原來的系統積累更多技術債。如果重寫得不夠徹底,很可能也會犯之前一樣的錯誤。重構則是一個安全的逐步清理程式碼的系統性方式,同時也可以保持系統持續運作。
重構有開銷,而且有許多程式碼需要重構。為了在有限的資源下做出最好的選擇,必須有針對性地重構。當需要改動程式碼(比如修復bug或者新增功能)的時候,通常都是重構的好時機。把握這些重構的機會,就會讓程式碼更容易維護和使用。
10 總結
在程式碼需要修改的時候重構遺留程式碼。使用重構技巧有條理地進行修改。我們的思想應該由外部的質量監控轉移到通過重構來提升程式碼可維護性並降低軟體所有者的開銷上。
本文中心思想如下。
-
學習如何有效地清理程式碼,以償還技術債。
-
將為新功能建立容納空間和開發新功能分開,可以大大簡化任務,降低引入bug的風險。
-
更有效地清理程式碼,理解為什麼在構建軟體時要持續改進設計。
-
熟悉重構之後,自然會編寫出更整潔的程式碼。
重構,以及如何正確重構,是清理遺留程式碼的重要手段。重構是修改或更新遺留程式碼的第一步,同時,也要對新編寫的程式碼進行重構,防止它演變成遺留程式碼。重構也是學習現有程式碼庫的有效方法。