筆記:重構 — Chapter 1 & 2:第一個範例 & 重構原則

YH Yu
後端新手村
Published in
12 min readAug 3, 2019

系列文章:重構 — 改善既有的程式設計

  • Chapter 1:作者拿一個義大利麵條式的程式碼當作例子,實際示範了一次重構的過程、重點觀念和成果,讓我們先對重構有一個大概的體會。
  • Chapter 2:著重在重構的觀念上,也就是建構出一套重構原則的“心法”。後面章節所介紹的重構“技法”都是圍繞著這些原則來展開。

本文會用以下順序,重新整理這兩章的重點:

  1. 重構的定義
  2. Why:為什麼需要重構
  3. When:什麼時候適合重構
  4. How:如何進行重構
  5. Problem:關於重構的常見問題

重構的定義

我們常常聽到工程師說自己正在做重構,或是某段程式碼需要重構。但重構倒底指什麼,不同的人可能有不同的解釋。為此,作者首先下了一個非常明確的定義:

Refactoring:a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.

根據這個定義,我認為重構有一個重點,兩個目的:

  1. 重點:不會改變程式碼的外在行為(observable behavior)。如果有人重構了某段程式碼,而導致原有功能被破壞。那麼他肯定不是在重構,如果方法正確,在整個重構過程中,程式碼都是可以穩定運作的。
  2. 兩個目的:讓程式碼更容易理解(easier to understand)和修改(cheaper to modify)。

聽起來很直覺,但似乎有點抽象。作者接下來用非常具體的方式,有系統地分析重構的各個面向。

Why:為什麼需要重構

  1. 使程式碼更容易理解
  2. 改善架構,更容易修改
  3. 提升開發的效率

使程式碼更容易理解

Any fool can write code that a computer can understand. Good programmers writes code that humans can understand.

寫程式的過程中,很大一部分時間是花在閱讀程式碼,這些別人(或是以前的自己)寫的程式碼越容易理解,我們的工作就越有效率。

改善架構,更容易修改

The true test of good code is how easy it is to change it.

以一個常見的重構手段—將重複程式碼提到共用函式為例。往後相關的邏輯需要變更時,便只需要修改一個地方,不需要在整個專案中逐一檢查。

反例就是四散的重複程式碼,由於很難一眼看出有多少地方需要修改,也不確定改了有什麼影響。結果就是大幅度地拖慢了我們開發的速度,以及增加日後除錯的難度。

提升開發的效率

易讀的程式碼讓我們快速進入狀況,也更容易發現或是定位 bug 。良好的架構則是讓修改程式碼的過程更快更穩定。最終結果就是開發效率的提升。

可以說,“花時間”重構的最終目的其實是為了“節省時間”。因為工程師普遍很懶,所以我們願意投資額外心力,打造各種工具來減輕日後的工作。重構也是一種投資未來的工具,而且隨著經驗的累積,重構的速度會越來越快,效果會越來越好!

Source:Refactoring Chapter 2,良好的設計讓我們可以維持高效率的開發

When:什麼時候適合重構

最基本的原則—事不過三(The Rule of Three)。第一次做某件事,沒問題。第二次再做一次類似的事,也許還能接受。但是第三次,請重構!

除此之外還有下列幾個主要的重構時機:

  1. 準備修改程式碼前
  2. 試圖理解某段程式碼時
  3. 路過看到(童子軍法則)
  4. 程式碼審查(Code Review)
  5. 反例:什麼時候”不該”重構

準備修改程式碼前

無論是要新增功能或是修復 bug ,在開始前,先實行重構可以讓整件事的效率大幅提升。書中舉了一個穿越樹林的例子作為類比:

在雜亂的架構下修改程式碼,就像筆直卻緩慢地穿越樹林。而先花點時間實行重構,雖然看似有點繞路,卻往往能更快到達目標。

試圖理解某段程式碼時

閱讀程式碼時,可以試著在理解片段後,將它抽出成獨立的函式,並根據意圖給予良好的命名。反覆進行這個動作,可以幫助我們釐清整段程式碼的邏輯,下次閱讀這部分時就不用再重複整個抽絲剝繭的過程。

路過看到(童子軍法則)

When programming, follow the camping rule: Always leave the code base healthier then when you found it.

像是看到重複的程式碼、過長的函式,甚至只是覺得一個變數可以命名得更好等等。只要有助於提升程式碼的品質,就應該養成“順手”重構的好習慣。若團隊中所有人都有這樣的共識,累積下來的成果會非常可觀!

程式碼審查(Code Review)

相較於單純“看過”待審查的程式碼,實際動手重構這些程式碼可以更為瞭解它們的意圖、有機會找出更好的寫法,以及給出更具體的建議。

作者非常推薦結對程式設計(Pair programming),也就是一個人實作,一個人審查,雙方可以持續進行討論,並將重構融入到這個過程中。

反例:什麼時候“不該”重構

我們或許常聽到『專案的時程這麼趕,根本沒時間重構。』這樣的說法有道理嗎?

You have to refactor when you run into ugly code—but excellent code needs plenty of refactoring too.

我們不該安排時間“特地重構”。因為程式碼往往沒有所謂最好的寫法,只有當下最合理的選擇(trade-off)。而需求永遠在變動,就算當下重構到很滿意,可能下個月再回來看又覺得不夠了。

因此,相較於預先排定好任務來“專心”重構,更好的做法是前面提到的幾個時間點,將重構變成日常開發的一部分。

以此作為延伸,許多團隊會將重構和功能實作的程式碼分離,方便個別提交(commit)和審查。但作者認為這不是一個好的做法。重構和實作間應該會有很明顯的上下文關係(e.g. 在實作功能前先重構),分開來處理反而會失去了這種連結。

下列是其它作者不建議重構的時機:

  1. 不太會再接觸到,也不需要理解的程式碼。可以單純視為 API 來使用。
  2. 只是需要增加一點點功能,重構成本卻很高的程式碼。可以考慮暫時先用 workaround 的方式解決,再觀察看看。
  3. 不確定如何重構,或重構後會不會真的有所幫助的程式碼。可以等之後有時間再進行一些實驗性質的重構。
  4. 重構的成本高到不如重寫(rewrite)的程式碼。

簡單來說,若重構沒辦法提升我們的開發效率,那麼可以考慮不重構。

How:如何進行重構

  1. 自我測試(Self-testing)
  2. 重構的過程中,程式碼是穩定的
  3. 兩頂帽子(The Two Hats)
  4. 程式碼的效能(Performance)
  5. 迭代的架構:Yagni

自我測試(Self-testing)

根據定義,重構本身不應該改變任何程式碼的外在行為。在進行重構前,最重要的就是要確保程式碼有足夠的測試保護。這樣我們才可以肯定在重構的過程中,沒有造成任何的破壞。

這些測試本身必須是能夠自我檢驗的,也就是只需要定義好預期的輸入和輸出後,程式就可以反覆不斷地執行測試、統計結果。整個過程都不需要人力去介入。

為程式碼添加測試並不是一種“選項”,而是開發流程中必備的步驟。若一個專案想要擁有良好的持續整合(Continuous integration, CI)流程,這些測試更是不可或缺的基礎。

重構的過程中,程式碼是穩定的

我們或許常聽到別人說『這邊我重構到一半,這個版本還不能用。』

但正確的重構過程,應該是由一系列連續的小步驟組成(本書中,對每個重構技法的步驟、實作,都有完整的描述)。重構並不是隨興地搜尋、取代程式碼。我們可以(也應該)在每完成一個步驟後,進行編譯、通過測試,並且提交到版本控制系統

兩頂帽子(The Two Hats)

雖然重構和開發功能是相輔相成的,但我們不會在同一時間做這兩件事,而是頻繁地在兩者間切換(就像交換戴著兩頂不同的帽子)。

在進行重構時,我們不會去改變程式碼的外在行為。就算是不小心看到 bug ,也不會順手偷偷修掉。而除非這段程式碼先前缺乏測試,否則我們也不會在此時加入新的測試,只注意重構過程中不要破壞到現有的測試。

而在開發功能或修復 bug 時,第一步就是為功能加上測試。然後根據測試的結果來觀察功能的完成度。這時候我們專注在功能的開發上,不會看到原有程式碼可以重構就順便改掉它。

反覆、快速地在這兩種模式之間切換。比起三心二意地一下做這個,一下做那個,一旦發生問題就可以很快速地定位、修復,在開發上更有效率。

程式碼的效能(Performance)

重構的過程中,我們有時候會覺得程式碼反而變多了。像是多呼叫了一個函式、多跑了幾次迴圈等等。這會造成效能問題嗎?

以作者的經驗來說,我們對於效能的直覺往往是錯的!這些改動在實際的運作場景上,可能根本看不出差異。而且就算真的有效能問題,重構過後的程式碼反而可以讓我們更容易去定位、調教程式碼的效能。

因此建議的流程是:

  1. 放心重構,在過程中先不要管“直覺上”的效能問題
  2. 重構完成後,用專門的工具做效能分析。
  3. 如果真的有效能問題,定位並且調教相關的程式碼。

迭代的架構:Yagni

設計程式碼架構的一大難題是,常常預留太多的彈性卻發現最後根本沒有用到,徒增無謂的複雜度。我們好像永遠無法準確預測未來可能的變動,到底架構的彈性如何拿捏才能算是“剛剛好”?

作者對這個問題的答案是“You aren’t going to need it(Yangi)”,也就是只需要根據當下的需求,做最簡化的設計。除非真的很確定未來可能的變動,否則不設計猜想中的彈性。

Yangi的設計方式不代表捨棄良好的架構。正好相反,這種開發方式需要將程式碼的設計與重構相結合,讓架構用穩定的迭代來成長。

Problem:關於重構的常見問題

  1. 程式碼的所有權(Ownership)
  2. 分支(Branch)
  3. 遺留程式碼(Legacy Code)
  4. 資料庫的重構
  5. 該怎麼跟主管說

程式碼的所有權(Ownership)

當重構修改到程式碼的介面時,若呼叫方也是自己那沒什麼問題。但若是由外部呼叫,甚至無法確定被誰使用時,情況就會變的有點棘手。

常見做法是保留舊有的的介面,然後將內部實作轉接到新的介面,再加上已棄用(deprecated)的標示來提醒呼叫者。缺點是程式碼的複雜度會提高,而且很難確認何時可以真正移除。

因此作者認為,應該讓團隊中(甚至跨團隊)的每個人,都有權限修改、提交不同專案的程式碼。若需要修改到共用的介面,便可以修改呼叫端的程式碼並且提交 PR(pull request)。

分支(Branch)

我們以往很習慣在開發時新建一個功能分支(feature branch),等待功能完成後再合併回主線。功能開發短則兩三天,多則好幾個禮拜。在分支獨立作業的時間越久,合併回主線的難度就越高。

但重構常常會對基礎的程式碼,做大量的小修改。白話來說,就是很容易影響到團隊中的其他人。這可能會讓我們在重構前多了一層疑慮。

持續整合的開發方式可以改善這個問題。團隊成員應該更頻繁(甚至是每天)和主線整合。也就是持續地將主線的變動拉回自己的分支,然後再將結果送回。全部成員都盡快知道程式碼的改動,就能降低合併的難度。

持續整合並不是一件容易的事,必須要隨時確保主線在健康(可以運作)的狀態。我們必須將比較大型的修改切成較小的區塊,並且加入穩健的測試。

遺留程式碼(Legacy Code)

對於遺留程式碼來說,重構是理解、整理它們的好方式。但這些程式碼往往缺乏良好的測試,在設計時也沒有考慮到架構是否易於測試,導致想要加入測試困難重重。

想要解決這個問題沒有捷徑,作者推薦 Working Effectively with Legacy Code 這本書。另外,即使已經成功建立起測試,最好也是一次重構一點(童子軍法則),不要急著翻新整個專案。

資料庫的重構

大致上和一般程式碼的重構沒什麼不同。唯一的差別在於,應該將重構的過程,拆分成多個階段發布。我們可以觀察每次變動後上線的運作情況,若有問題發生,比較好回復到先前的狀態。

資料庫的重構可以使用平行(parallel)的方式實作。像是想要重新命名欄位名稱,可以拆成下列階段來發布:

  1. 加入新欄位,但不使用
  2. 讓舊欄位的資料變動,也一併更新到新欄位
  3. 將讀取舊欄位的程式碼,逐一切換成新欄位
  4. 確定已經用不到舊欄位後,移除它

該怎麼跟主管說

主管或客戶若不懂得重構的價值,甚至有所誤解。例如,需要重構是因為程式碼太爛、有時間重構表示任務的時程太輕鬆等等。那麼最好的方式就是不要說!

還記得重構的終極目標是為了提升開發的效率,重構本來就應該融入日常的開發流程中。只要能夠準時地提交高品質的程式碼,並不需要向主管提及任何跟重構有關的事情。

總結

最後用一個條列重點的方式,當作自己的重構原則檢查表:

如果這篇文章對你有所幫助的話,歡迎拍手讓我知道,最多可以拍50下喔👏👏

--

--

YH Yu
後端新手村

Ever tried. Ever failed. No matter. Try Again. Fail again. Fail better.