這是一篇讀書心得,主要分為兩部分:重構的核心概念,以及實際的重構solution。我會將重點著重在前者,後者若有時間的話會列出我覺得重要的部分。
(範例程式碼寫上去)
幾個重點:
簡單來說,重構是改善已經寫好的設計。這聽起來有點怪,因為大多數軟體都是「先設計、後工程」,但是隨著時間過去,程式碼不停被修改,會使得程式碼從依循既有的設計,變為隨意更動的混亂。重構則是將不良/混亂的程式碼抽出來,重新修改為良好的設計。
更精確地說,重構是指「對軟體內部結構進行變動,在不改動軟體可見行為(observable behavior)的前提下,提高它的可理解性,並降低修改他的成本」
靈芝的好壞取決於多醣體。 程式的好壞取決於它多清楚:夠清楚,代表閱讀的『人』能更輕鬆地讀懂程式碼,找到哪個函式/元件是需要修改/新增的。糟糕的程式碼則難以閱讀、修改時需要重新閱讀的時間極高、且容易改壞掉。
在我看來,好的程式碼有三個要件:
- 易讀性(下一個接手的工程師可以看懂你每行code在幹嘛)
- 分離性(一個複雜的code裡一定包含很多功能,而好的code能將這些功能拆成不同子函式/元件,每個子函式/元件僅肩負單一任務)
- 複用性(子函式/元件可以被複用,而不用再複製同樣的程式碼去處理幾乎同樣的任務)
如何有效地重構,關鍵在於採取小步驟:將重構的整個工程,拆解成數個單一任務,並盡可能在每個任務中加入測試。好處有二
而且對工程師而言,專注於更小的改動,能夠有更即時的feedback,更能掌握重構的進度,有益身心健康(?。
重構的最終目標不是寫出「簡潔的程式碼」,而只是為了提升開發功能與修復bug的速度。
我自己的感受:當我發現目前的程式碼難以閱讀、有重複的程式碼在做同一件事時、未來可能有部分元件/函式可以復用時,就是重構的好時機。
作者有提到一個重要的概念「營地原則」:重構不是要求程式碼能夠一次重構到位,而是至少讓它比原本更好一點。就像營地不可能一晚上從素亂不整變得整潔有序,但至少可以讓它稍微宜居一點。
預備性重構:加入新功能前,重構既有的函式
在加入新功能或修復bug前,可以先花點時間進行重構:比如先檢視既有的函式能否復用,若發現有相似的函式,可對既有的函式參數化,並用於新功能中,減少重複造輪子。
理解性重構:重新命名,讓程式碼更容易被理解
打掃性重構:可以理解程式碼在幹嘛,但他用了很糟糕的方式在運用(比如多個重複的程式碼、邏輯無謂的重複,函式/元件的任務不清楚),這時就可以進行重構。
計畫性重構與長期重構:
雖然大部分的重構都是在開發的過程中完成的,但不可避免的,還是會有大規模的重構需要執行。作者認為,當這種情況發生時,可以採「重構不會破壞程式碼運行」的前提下,小步驟的修改。
重構是對既有的程式碼進行修改,與其相對的是實作新功能,作者對兩者的關係有以下見解:
其實不一定要把重構放在同個PR,因為如果重構所牽涉的範圍太大的話,反而會導致code review要花的時間過多,導致PR卡住,拖延開發進度。
但如果牽涉的範圍很小的話,是可以放在同個PR的,但仍建議不要放在同個commit。
僅因為「明確的需求改變」而調整程式碼架構。作者認為是在加快未來實作新功能的速度
前面談的都是程式碼重構的原則與好處,現在則是開始介紹「遇到什麼樣的情境可以著手重構」。
作者認為,當程式碼出現這些徵兆時,就可以著手進行重構。作者使用了「異味」(Smell)形容這些
時間。
可以想成,待重構的程式碼其實都有一些hint在告訴我們「該做重構了」,「異味」就是指這個hint。
以下將介紹這些異味出現的情境,以及一些解決異味的方法
Mysterious name
change function declaration
, rename variable
, rename field
等。重新命名不僅只牽涉到改變名稱而已,如果無法想出好名稱的話,通常代表著程式碼本身的設計不良,這時就需要搭配其他重構手法,比如簡化程式碼等。Deplicated Code
extract function
後在不同地方呼叫它;如果是兩個相似的函式的話,則可以先透過Slide statement
整理程式碼,並考慮參數化的可能性;如果是不同subclass中有同樣的method,則可以透過pull up method
提取至basic class。Long Function
extract function
:從大函式中,將一個或多個但重複的程式碼提取成一個函式,並在大函式中呼叫它。replace Temp with Query
: 如果暫時變數過多,可以將他提取變成函式,減少傳入的頻率,避免傳入過多變數給予提取出來的函式,閱讀性提升不高。Introduce Parameter Object
& Preserve whole Object
:簡化一長串的參數,詳見Long Parameter List
Replace Function with Command
(這個我真的看不懂)Long Parameter List
Replace Parameter with Query
Preserve Whole Object
introduce Parawmeter Object
結合成一個資料結構Remove Flag Argument
Global Data
Encapsulate variable
假設有一個全域變數長這樣:
這個全域變數是可以被其他所有程式碼access的,所以可以
作者建議有兩種封裝的方式:
封裝方法,讓讀取、修改被轉為一個函式,如果要更動全域變數,透過這種函式執行,能被更容易的追蹤與修改。
這樣的好處在於:
- 讀取、修改由函式負責且同一個地方管理(defaultOwner.js
),能更好監看資料的變化與使用方式。
- 如果要修改讀取、修改的方式,只需要改一處的函式就好。
封裝值,讓修改時僅修改副本的值,甚至不可被修改
Mutable Data
Encapsulate variable
,控制修改的行為可被集中管理於一處。Split Varibale
:如果有變數被儲存成不同東西,可以將它分開。我們常透過變數保存程式碼執行的結果,以方便我們引用。但如果一個變數被重新複值,代表這個變數所肩負的任務超過一個。而肩負多重任務的變數,容易讓其他工程師一頭霧水。
假設有個程式碼長這樣:
如果price被重新assign的過程,被包裹在很隱晦的地方,光看console.log,很難得知到為什麼兩次的temp會不一樣。而取
作者建議改成這樣:
這麼改有兩個好處:
1. 原本一個變數要處理兩件,現在是兩個變數個別處理,兩個變數的功能變得更單純。
2. finalPrice不會修改到原本的basePrice,進而造成難以預測的bug。
但必須注意的是,如果變數為收集變數(比如說 i = i + something
),這種就不要拆開,原因在於i本身的功能為收集總和、字串串接、將東西加入一個集合(比如說陣列)等,拆開反而毀掉了i原本的用途。
Slide Statement
、Extract Function
:讓處理更新的函式只處理更新,其他的邏輯、函式都搬到外面或分開。Seperate Query from Modifier
:呼叫變數時不要執行具「修改變數」等副作用的函式。函式可以分成「有副作用」與「無副作用」的函式:無副作用代表函式只會依據import回傳output,不會修改函式外部的任何程式碼。有副作用則在執行函式,除了回傳output,還會對函式外部造成影響。比如說叫出console.log,就是一個很常見的side-effect。作者認為有時候side-effect其實是一個函式的主要功能(main effec,但可以的話,盡量把無副作用與有副作用的函式分開。比如說
Remove Setting Method
:移除set方法,減少變數可被直接修改的機會。Replace Derived Variable with Query
:減少可變資料的作用域假設有一個class叫做ProductionPlan,用於紀錄手機店的進貨數量與品項,並可以得出Derived Variable totalAmount
,為所有進貨手機的數量
作者建議可以改成這樣
這麼做的好處在於:
1. totalAmount的計算來源更清楚:以前我只知道他是this._amount
,但this._amount
是什麼我不清楚,現在我知道是this._adjustments
藉由reduce累加的結果。
2. 避免資料來源改變導致的故障:totalAmount原本是來自於this._amount
,而this._amount
又是來自於anAdjustment.amount
。但如果this._amount
又因為不知名的原因被修改了,可能會導致totalAmount的結果不如預期。改動後的totalAmount則是直接計算this._adjustments
的數值,不受this._amount
所影響,可變變數所造成的風險較小。
3. 當然如果你能確定資料來源(this._amount
)是不會變的,那不用做這個改動。