Refactor

10 January 2024

宗旨

這篇文章宗旨在紀錄如何處理legacy code, 對公司內的legacy code, 維護成本過高(因為沒人看得懂, 很難改得動). 因此記錄一些Refactor技巧讓legacy code能夠變得更好維護以及擴充

看完文章後認為的幾個要點

  1. 增加可讀性
    • 做法包括method extract, 不要用tmp variable等等的
  2. 增加可修改性
    • 避免過大過長的method, class,導致後續不好修改, 把它拆小,並且盡量減少dependency(function, class之間)

Reference

Refactor Guru

Long Method 處理法

legacy code往往因為各種擴充而使method變得非常長而難以閱讀維護.

修改方法

Extract Method
盡量讓每個method都不要太長, 當一個method/function發現寫的行數變多時, 可以把他extract成另一個function, 盡量讓每一個function只做一件事. 並且透過良好的命名來避免comment這個function的目的, 這樣會有點functional programming的風格, 一個function內其實就是不斷的call function來完成各種事, 而function name又可以知道這function在幹嘛.

一些小細節
function argument不要太雜太針對,越general越好, 因為這樣會變成要call這function不但不容易(要先建好很多的變數, 很多變數可能到後來也不知道是幹嘛的), 另一方面會出現這種狀況就是沒做好封裝, 封裝應該要有一個好的interface來讓使用者使用該function.

Duplicate code會導致bug fix的時候要找到程式碼裡所有這個邏輯的地方都fix, 這很容易遺漏掉, 也不好維護. extract method也能避免duplicate code,之後要維護也相對簡單一些, 反正他就做簡單的事, 邏輯也沒有散落各地.

Seperate Modifier from Query
盡量避免出現stateful的狀況, 否則之後很難debug, 例如裡面使用到static, 或者修改某些global, heap裡面的值, 如果extract出來的method是stateless, 那他是很珍貴的.

因此要盡量避免同一個function有query跟修改的功能,這樣會讓function功能太多太複雜,而且call的時候有dependency。 query時就不該修改任何東西,修改東西的task獨立另一個function做。

使用OOP封裝邏輯

另一種long method常見於許多的if / else, 或者switch/case, 根據不同的狀況使用不同的function做事, 其實做完extract method後, 如果每個if/else block都變得很小, 那可讀性就高很多了, 那再更近一步避免過多的if,else, switch case 就是使用Design pattern或unordered_map的方式封裝邏輯:

使用unordered_map, key的方式來根據對應的key值呼叫對應的function call來處理事情 (Strategy pattern).

使用Design Pattern, 例如strategy pattern, factory pattern來根據需求call不同的function, 可以消除if/else的狀況, 新增corner case要handle也可以漂亮,輕易的去新增

Nested condition

在legacy code疊床架屋下也很容易發生很長的nested if / else condition, 這會讓城市可讀性變很低,作法就是把他extract, special handle的抽取出來成一層if, 然後可以early return/break

修改方法

Decompose Conditional
long method裡面通常有一個狀況是if/else for特別多, 而且條件複雜, 此時就可以把condition的邏輯extract出來, 用一個IsXXX()來判斷, 這樣會增加可讀性, 也能共code, 又或者多個condition裡的block是做相同的事, 可以把這些condition一起封裝起來, 這樣也可以避免duplicate code, 又可以保證這些condition下都是共同行為, 不會之後變得更複雜.

Consolidate Conditional Expression

如果兩個condition要執行的邏輯一樣,可以把這兩個condition合併 就不用分兩個if/else了

Replace nested condition with guard clause

nested condition有時候最終結果類似return 某個東西,那就可以把這個複雜的condition path萃取出來,盡量避免多層if else

Switch

在legacy code中也很常使用switch/case來根據不同的enum type執行不同的行為。這其實不是一個好的做法,一來是這樣也是疊床架屋,可能邏輯會散落各地,產生duplicate code的狀況,然後如果對於每個type甚至同個type不同狀況要做不同handle, code會變得很複雜。

修改方法

使用polymorphism取代switch case, 對每個enum type建立一個class object,並呼叫該method來執行。 這樣符合兩個principle

  1. open/close principle, 以後要handle新的data type也會變得很容易新增, 而不用每個switch case都要改
  2. Tell Don’t Ask principle: 不是根據狀況再決定要怎麼handle, 而是直接呼叫該enum type的class object method, 讓他決定具體他要走什麼flow
  3. Replace Typecode with Class or strategy pattern

Tell Don’t Ask principle

這樣可以做好更好的encapsulation, 等於class object根據自己的data member決定要怎麼執行,而不是在外在的code加一堆判斷或variable來決定要call哪個method或者要傳什麼參數進來,後者的做法問題在於maintainability, 當一個地方要改,等於一樣所有地方都要改,有遺漏沒改到的flow, condition就會是potential issue, 但如果都encapsulate進method裡,那等於要加condition, 做修改只要在class method裡面做就行,code也會看起來相對乾淨,不用在call method前又做一堆有的沒的判斷 (例如要先get object的data看他的值在什麼範圍內才call method, 這種邏輯可以包在method裡)。

Reference: LinkedIn post

Replace Typecode with Class, strategy pattern

常常我們swtich case會使用type code,來根據type code的值執行case statement, 這部分會有幾個問題:

  1. 可能會有多處都用類似的switch case, 導致duplicate logic, 就算盡量共用code, 還是很容易導致每個地方case statement的行為有因為些微差異,而不好refactor, 也很容易有bug
  2. 不scalable, 如果今天要加某個新的case, 就要修改原code, 然後可能就會有地方忘了加到, 而且要修改遠有的code容易變成long method.

可以把switch case的type code改用成一個class, 使用class member來存typecode, 在根據typecode種類建立多個subclass, 因此可以透過factory pattern根據當前type建立對應subclass, subclass再去執行該method就可以避免switch case的問題。

Long Parameter

在函式的開發過程,往往因為需求而家了更多的parameter上function interface, 結果就是導致function會有愈來愈長的parameter list

Large Class

Class同method/ function遇到的狀況, 因為功能越加越多, 所以class到後來都非常巨大, 修改目標依然是讓他可讀性增加, 可擴充性, 可修改性增加, 減少dependency, 不然code會改不動.

修改方法

Extract class
一個class盡量也不要太多的功能, 多餘的功能, 或者可拆的功能把它拆到另一個class(例如已經完全是另一個功能或者是某些特定的case, corner case), 然後這兩個class盡量只有單向dependency(雙向也可以, 但單向整個code的coupling比較小).

Extract subclass
在class裡面, 常常會有很多if/else專門處理某種edge case,特殊情況(例如某個field值等於多少時才走某段code),如果再多個method裡面都有同樣的狀況時, code會變得非常難懂. 因此對於這些edge case/ corner case可以使用繼承parent class的方式, 把這些if else的edge case改成sub class(derived class)處理, 這樣分工就更精細好讀.

Pull up Field/Method
核心概念在於class很多常常有重複邏輯, duplicate code. 所以要減少這些發生.
對於很多子class共有的field, method, 可以把這些移到base class,在做virtual的overwrite, 繼承.
對於constructor很多重複的部分也可以使用super的方式讓邏輯盡量在base class, 不要copy & paste

Pull down Field/Method

相反的 為了減少dependency, 增加maintenance, 對於某些在base class的method, 如果只有少數的derived class會用到, 反而可以把這些method/field搬到derived class.

Collapse Class Hierachy

當derived class / base class的功能幾乎一模一樣時, 可以考慮把他們合併, 這樣可減少複雜的繼承關係

Replace Inheritance with delegation

Delegation的意思是在原有的class中宣告一個field存另一個class object, 要的時候直接拿來用. 優點在於一個class變得過大,有太多功能. 把其餘功能寫在另一個class, 然後hold這個class, 這樣可以loosly couple, 也容易修改擴充. 但假設class對delegate object dependency太高(意即原class幾乎什麼事都要靠delegate object來完成), 那可能代表原class跟delegate class是在做類似事情, 此時直接把原class做成subclass繼承delegate的class就好.

Parameterized method
當有複數個method/function在做類似事情,可以視情況將他們合併成一個general method, 然後傳parameter進來做事, 此parameter應該只影響最後算出來的值,不該作為control flag來控制function的flow。 也要視情況避免又讓general method變成long method。

Remove Setter
如果有些data member應該在建立時就設定好後來就不改,那就可以把這些data member的setter method移除,避免誤call導致bug, 也可以減少method數目。

Hide method
對於internally使用的method, 盡量設成static function或private method, 確保一個method, function的用途單一而且不會被到處拿來使用導致dependency,程式複雜度變高.

Primitive Obsession

  1. 寫code時常常因為方便而使用primtive type來代表某些複雜的東西, 例如就很常使用int, 例如mode=1,2,3,4等等方式來代表做不同事情
  2. 又或是API input/output規劃時, 都是用string來傳各種資訊, 反正交給caller parse, 然後再依據狀況做不同的code flow或者建立不同的object

這種問題在legacy code也很常見, 事實是隔了幾年後看到mode, return string會變得不知道這段code的caller期望得到的是什麼, 又或者mode代表的意思是什麼. 而且隨著caller越多, 該function的mode會疊床架屋上去, 又或者

好的寫code習慣

不過度依賴comment
雖然comment能夠介紹此code在幹嘛, 但comment在維護上也是成本, 因此comment多用於function declaration定義input/output以及一些注意事項(.h檔內), 在definition地方能夠的話盡量少用comment, 不然會擔心之後出現錯誤的comment, 與程式不符的comment, 或者根本不知道在幹嘛的comment.

  1. 除非code難以看懂, 否則不用特地comment這段code在幹嘛(大家都很聰明的)
  2. 更重要的事,為何要加這段code. 如果沒註解為何加這段code,以後維護的人, 如果想對這段code做修改, 例如看到這段code裡有怪怪的地方, 因爲沒辦法確定創始人的意圖, 沒辦法確定是不是能移掉或修改這段code.

Don’t use tmp variable
在寫code時,我們有時候會建立一個tmp variable來hold這個值,他可以拿來重複運用存一些中間運算產生的值,簡言之就是把算出來的值暫時存在這裡。

這有兩個極大的問題,而讓這個function很難refactor:

  1. tmp variable名字很爛,會不知道這個variable的目的跟意義是幹嘛的
  2. tmp variable可能到處被拿來用,違反每個component就負責一件事情的核心概念。

每個function/variable盡量只有一個功能

一個變數盡量一個固定功能,不要拿來做多個用途 例如前面拿來存某個暫時的直, 後面又歸0再拿來重複使用存另一個暫時的直, 這會導致code很難refactor, 會阻礙extract method, 因為有個tmp變數橫跨多個程式區段, 怕extract有side effect等等.

Don’t use magic number
magic number常會在維護時不知道他的用途,給他名字,就算他是常數,讓大家知道這個常數的意義