Working Effectively with Legacy Code — Sprout Method

Hardy Liu
4 min readFeb 13, 2017

--

開始有機會處理商業邏輯較複雜而且規模較大的專案後,深深地體悟到與 legacy code 和平相處也是一門學問。因緣際會下在某次公司內部的 sharing 中講者提到了 Working Effectively with Legacy Code 這本書,這本書裡面提供了一些方法如何與 legacy code 和平相處,接下來幾篇文章會慢慢整理一下書中提到的方法與一些範例。

什麼是 legacy code,作者 Michael C. Feathers 在本書的開頭就為 legacy code 下了定義:

To me, legacy code is simply code without tests.

因此這本書就是提供一些方法,如何在沒有測試程式的狀況下去安全的修改程式碼,並且漸漸的讓專案的測試覆蓋率提升。

Sprout Method

在維護既有的專案時很常遇到的一種情況,在時間不充裕的前提下,要去修改已經存在的功能,例如下面這個例子,程式內有一個 postEntries 的 method:

func postEntries(entries: [Entry]) { 
for entry in entries {
entry.postDate()
}
transactionEntries.append(contentsOf: entries)
}

我們希望加入檢查 entries 是否已經存在 transactionEntries 裡,如果不存在在執行 postDate 然後加入 transactionEntries 裡。我們可以增加程式碼來判斷項目是否已存在在 transactionEntries 裡:

func postEntries(entries: [Entry]) {
var entriesToAdd = [Entry]()
for entry in entries {
if transactionEntries.contains(entry) {
entriesToAdd.append(entry)
entry.postDate()
}
}
transactionEntries.append(contentsOf: entriesToAdd)
}

這雖然是很簡單的變更,不過這樣的寫法,新的程式碼跟舊的程式碼混在一起比較難確保程式的正確性。但是如果改成下面這種寫法:

func uniqueEntries(entries: [Entry]) -> [Entry] {
var entriesToAdd = [Entry]()
for entry in entries {
if transactionEntries.contains(entry) {
entriesToAdd.append(entry)
}
}
return entriesToAdd
}
func postEntries(entries: [Entry]) {
let entriesToAdd = self.uniqueEntries(entries: entries)
for entry in entriesToAdd {
entry.postDate()
}
transactionEntries.append(contentsOf: entriesToAdd)
}

我們將新加入的程式抽離出去變成另外一個 method ,原本的 postEntries method 增加了一個新的變數其他都幾乎維持不變。這樣的做法除了可以讓新舊程式碼盡可能不要混淆在一起以外,更重要的是在時間有限的情況下,可以先不用幫舊的程式補齊測試,但卻可以替新修改的程式碼寫測試程式(或是使用 TDD 完成新的程式碼修改),像上面這樣的範例就是 Sprout Method。

使用 Sprout Method,在修改現有的專案的同時,雖然無法將 legacy code 的數量降低,但也可以確保新的變更不會變成 legacy code ,當專案時程緊迫時會是一個蠻有幫助的方法。

--

--