🎯 什麼情境該想到我
- 要對沒有測試的舊碼動刀,先想清楚「測哪些方法才夠」。書上說最簡單的答案是為所要修改的每個方法都寫測試,但「一個地方的改動可能會影響到其他地方的行為:除非有測試『坐鎮』否則我們可能永遠也不知道自己的修改造成了什麼影響」(p.127)。
- 要加一個新特性,卻發現得同時改三四個緊密相關的類,每一個都得花好幾個鐘頭才進得了測試用具——書問「真的必須得一個一個的來解開所有這些依賴嗎?那可不一定」(p.144)。
- 面對特別錯綜複雜的舊碼,得先花點功夫「推測一下代碼修改會產生哪些影響,以便找到編寫測試的最佳地點」(p.127)。
- 想判斷一段設計好不好——影響結構圖若「散」、終點很多,通常代表不好測也不好懂(p.130、p.141)。
⚙️ 怎麼用
1. 畫影響草圖
書把這種圖稱為影響草圖(p.130);譯註提到「後文也有稱『影響結構圖』、『影響結構示意圖』的,意思一樣」(p.130 註)。
作圖規則書上寫得很輕:「為每個可能會被影響到的變量以及每個返回值可能改變的方法畫一個單獨的橢圓……並從它們出發畫一個箭頭指向那些因它們的改變而在運行期改變的東西」(p.130)。變數來自同一個物件或不同物件都不重要。
畫的時候「你得確保找到了所考察的類的所有客戶端。如果你的類有一個基類或派生類,那麼得注意一下它們裡面是不是還有沒有被注意到的客戶代碼」(p.134)。
2. 兩個推理方向
- 往回推(找原因):從某個地點值的異常出發,推斷是哪些物件對它產生影響——書說這其實是個調試問題,「得從問題的現象一路推到它的源頭」(p.132)。
- 前向推測(找測試點):書用整整一節(11.2「前向推測」)講這個方向。寫特徵測試時「這個過程是反過來的……面對一組對象,試圖搞清如果它們停止工作的話『下游』會發生什麼情況」(p.132)。第一步是「推斷出哪兒可以探測到我們的修改所帶來的影響」,知道在哪能探測到之後,「在編寫測試的時候便可以在這些地方進行選擇了」(p.137)。
3. 記住影響傳播的三種途徑(p.138)
- 調用方使用被調用函數的返回值。
- 修改傳參傳進來的物件,且後者接下來會被使用到。
- 修改後面會被用到的靜態或全域資料。
書提醒第 3 種是「代碼影響代碼的最難以覺察的方式」;第 2 種則因為多數語言預設傳的是物件的「句柄」而非物件本身,「任何方法都可以通過它們接受到的句柄來修改相應對象的狀態」(p.137–138)。書也提到「有些語言中也有其他途徑」,例如面向方面的語言(p.138)。
4. 六步啟發式(p.138,書中原文的尋找順序)
- 確定一個將要修改的方法。
- 如果該方法有返回值,查看它的調用方。
- 看看該方法是否修改了什麼值。是則查看其他使用了這些值的方法,以及使用了這些方法的方法。
- 別忘了查看父類和子類,它們也可能使用了這些實例變數和方法。
- 查看這些方法的參數,看看你要修改的代碼是否使用了某參數物件或它的方法所返回的物件。
- 找出到目前為止被你所找出的任何方法修改的全域變數和靜態資料。
推測時最重要的籌碼是「對編程語言的認識」——每門語言都有所謂的防火牆,即「能夠阻止影響繼續傳播的語言結構」,知道它們就清楚什麼時候不必穿越它們去追溯影響(p.138)。但書也警告:有些語言特性看似能保證「不會改」,實際上卻能被別的關鍵字繞過去(書舉的是 C++ 中修飾方法宣告的常性關鍵字,遇上父類中某個可變修飾的成員宣告就失效),「像這類能夠被『繞過去』的語言特性都要加以注意」(p.139–140)。書給的重點只有一句:「了解你所用的語言。」(p.140)
5. 找攔截點
「給定一處修改,在程序中存在某些點能夠探測到該修改的影響,我們把這些點稱為攔截點。」(p.145)
「最佳切入點莫過於先確定需要進行修改的點,並從這些修改點開始一路向外追蹤影響。每個可以探測影響的點都是一個攔截點,但並非每個都是最佳攔截點,在整個過程中你都需要自己進行判斷」(p.145)。
大多數情況下,最佳攔截點就是被修改類上的一個公共方法——「這類攔截點容易尋找,也容易使用,但有時候並非最佳的選擇」(p.147)。
6. 一次改多處時,退後一層去找匯點 ★
改一堆擠在一塊的東西時,書建議的辦法是「退一層測試」:「『退後一層』從而找到一個地點能夠同時給多處修改編寫測試。比如要對一系列私有方法進行修改,只需為某一個公有方法編寫測試就行了」(p.144)。
而書中所說在代碼中所能找到的最佳攔截點,就是匯點(p.144):
「匯點是影響結構圖中的隘口和交通要衝,在匯點處編寫測試的好處就是,只需針對少數幾個方法編寫測試,就能夠達到探測大量其他方法的改動的目的。」(p.149)
書說匯點就是「在單一地點探測所有影響」的地方,「在這類地點編寫的測試能夠覆蓋大量的修改。若能在設計中找到匯點的話,你的工作就會輕鬆許多」(p.149)。
這就是「要不要把相關的類都解依賴」的判準:與其給每個類分別寫測試,「更高效的做法是試著找出一個能夠用來刻畫這塊代碼的特徵的高層攔截點。這麼做的好處有兩點。首先我們需要進行的解依賴可能減少了,另外我們的『軟體夾鉗』所夾住的代碼塊也更大」(p.147)。有了刻畫這組類特徵的測試,重構就得到更多守護——改 A、B 兩個類的結構時,可以拿 C 的測試當作不變式(p.147–148)。
找匯點的第二個辦法:找出方法或類「被使用方式之間的共同之處」。某個方法可能有三個使用者,但這不代表三者的使用方式各不相同。關鍵問題是——「如果破壞該方法,在那個地方能否感知到?」如果它在一組物件上的使用方式都是一樣的,「則只需在其中一處測試即可」(p.150)。
7. 用簡化影響圖來反過來改設計
書示範了一個很小的改動:原本兩個公開方法各自直接讀取同一份資料,改成其中一個在內部改呼叫另一個,影響結構圖立刻變形——「這只是一個小小的改動,然而帶來的影響卻是顯著的」,因為測試外層那個方法時「也就連帶測試了」被它呼叫的那個(p.142)。結論是:「在消除了一點點的代碼重複之後,我們往往能夠得到一張『終點』更少的影響結構圖。而後者則往往能夠令你的測試決策更容易」(p.142)。
書也指出「衡量軟體好壞的標準之一便是,看看該軟體對外部世界的相當複雜的影響能否由代碼內的一組相對簡單得多的影響所構成。任何改動,只要能夠使代碼的影響結構圖簡單化,就能夠使其更易理解和維護」(p.130)。
匯點還可以拿來判斷設計好壞:「一個匯點其實就相當於一個自然封裝邊界。發現一個匯點就相當於發現了一個『漏斗口』,一大塊代碼的影響都得從這個口經過」(p.151)。影響結構圖也能用來從一個龐大的類裡發現潛在的新類——看圖中的自然封裝邊界,替邊界內那組方法/變數想個名字,那就是要分離出來的新類的名字(p.151)。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- 攔截點離修改點越近越好(p.147)。理由一是安全性:從修改點到攔截點,途中每一步「都好比是邏輯論證過程中的一步……論證過程中的步驟越多,我們就越難判斷論證的正確性」;理由二是離得近的地方安置測試通常比較容易。步驟一多,「通常你得在大腦裡面模擬代碼運行來確定一個測試是否覆蓋了某塊遙遠的功能」。
- 匯點是由具體的修改點決定的(p.149)。換一組修改點,匯點就可能不一樣;書的例子裡加了一個新方法後匯點「沒有絲毫變動」,但再加一處改動後就「不再存在一個單一的攔截點了」——此時把兩個方法合起來看仍算匯點,因為兩個方法比起要觸及的八個方法/變數還是經濟。
- 找不到匯點時:「辦法之一便是重新考量我們的修改點。問問自己是不是太『貪』了,考慮能否一次僅為其中一兩個修改點尋找匯點。最後,要是實在沒法找出匯點的話,那就按就近原則直接給每個修改編寫測試吧」(p.150)。
- 匯點的陷阱:在匯點寫的測試容易「緩慢但逐步地演化為『迷你型』的集成測試」,最終變成「龐大而笨重的、不知何年何月才能運行完成的『單元測試』」(p.152)。新寫的代碼要盡可能單獨而孤立地測;但既有代碼「情況就反過來了」,先切一塊下來用測試鞏固它,之後再為區域內每個類寫更窄的單元測試,「最終當這些單元測試全都完成,原先在匯點編寫的測試也就可以功成身退了」(p.152)。
- 高層測試不能替代單元測試:「高層測試雖說是個重要的工具,但並不能替代單元測試。而是為最終將單元測試安置到位而進行的鋪墊」(p.144)。
- 解依賴會破壞封裝,這是真的(p.143)。書坦承「打破封裝會令代碼中的影響推測變難」,但「一旦一個類有了測試用例,就可以使用這些測試用例來更為直接地進行影響推測」。作者的取捨很明白:「當它們真的發生衝突時,我會傾向於選擇測試覆蓋」,而且「封裝本身也並不是最終目的,而是幫助理解代碼的工具」。
- 在匯點圈出的安全區可能是假的:完成特徵測試後你在應用中造出了一個「小綠洲」,但書提醒「請當心,這片『綠洲』有可能只是一個虛幻的海市蜃樓」(p.152)。
- 這套推測「可以是非正式的,也可以稍微嚴密一些,借助一點草圖來進行」(p.143)——不必每次都畫全圖。書自己也說當年希望有 IDE 能自動列出影響清單,「或許有一天人們會開發出這樣的工具。但在那一天到來之前我們還是得學習如何在沒有工具的情況下僅憑大腦去推測代碼修改的影響」(p.128)。
🔗 相關工具
- 工具-遺留程式碼修改流程 —— 上層流程;影響分析是其中「找測試點」那一步
- 工具-特徵測試 —— 找到攔截點/匯點之後,在那裡寫的就是特徵測試(書中特徵測試見 p.153)
- 工具-打破依賴以便測試 —— 匯點的價值正在於「需要進行的解依賴可能減少了」(p.147)
- 回連 修改程式碼的藝術