你打開了一個原始碼檔案,心中馬上開始尖叫:「這是什麼鬼東西,根本就是個屎坑!」原諒我使用這麼粗穢的名詞,不過,我們真的是這麼稱呼這類程式碼,別說嗅出程式艘味(Bad smell)了,這根本是掐著鼻子都還擋不住臭味撲鼻,既然都跳進來了,那麼就好好想想看,該怎麼「喇賽」才喇得出有用的東西來吧!
重新編排程式
既然已經打開程式碼了,那麼接下來要選擇的就是,該從哪個地方跳下去,就算這程式看來完全混淆、雜亂無秩序,你還是得先找個跳下去不會馬上滅頂的地方開始,建議你採用些安全設施,像是版本控制工具,萬一真的滅頂了,還可以像遊戲Game over之後,選擇重新開始或接關;先寫個簡單的功能測試,確保在整理程式的同時,沒有破壞了什麼東西,不過,你最終還是得面對臭味薰天的程式碼。
既然程式碼會臭味薰天,多半表示它伴隨著混亂的編排,就如同編排混亂的文章或書籍令人難以理解其內容,想要理解程式碼做些什麼,第一步就得試著讓程式碼「看起來」有良好外觀,雜亂的程式碼最不能信任的就是既有的程式碼編排,如果編輯器或整合開發工具有程式碼自動編排功能,不用懷疑,就讓它自動編排吧!這時機器一成不變按照規則來編排,會比肉眼編排更為可信,這不單是讓程式碼可以容易閱讀一些,也可以避免隱藏類似Apple在2014的goto錯誤(https://goo.gl/WgFrMk)(重新編排後,就加上個花括號吧):
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
如同要為不分段的文章在適當地方按下Enter一樣,第二步,是在適當的地方為程式碼「分段」,每個縮排區塊的開始、宣告變數之後,都可能是個適當的分段之處,這有點像是在一團爛泥中畫下界線,接著就可以試著檢視各個段落中的程式碼。
試著在每個段落中,找出變數在哪些地方被改變了值,或者參考的物件狀態,如果已經辨識出變數的作用,思考一下現在的變數名稱合適嗎?如果不合適,試著重新命名變數,名稱如果不合適,多半是因為變數的使用橫跨了數個段落,試著將該變數的值或物件指定給新建立的變數,在程式碼段落執行完之後,再將新變數的值指定給原變數,看看這樣有沒有辦法,為新變數與舊變數取個適當名稱。
在重新編排程式階段,就只是重新編排,若發現了可改進程式的地方,切記等到編排結束再進行。修改編排也同時改變程式流程或功能,應當避免。
削減笨邏輯
臭味薰天的程式,多半有許多不必要的程式碼,這些程式碼都是未經大腦思考而寫下的程式,整理並移除這類程式碼,程式碼就會獲得大量的改善,而且程式碼被大量消減的同時,還能夠大幅增強整理程式的信心!往往地,第一個可以消減的程式碼,是顯而易見的「笨邏輯」,除了if(expr == ture) ...這類笨邏輯之外,以下實際案例也是常見的情況:
while(toCrawlList.size() > 0) {
if(maxUrls != -1) {
if(crawledList.size() == maxUrls) { break; }
}
...
這個程式碼可以整併為while(toCrawlList.size() > 0 && maxUrls == -1 && crawledList.size() != maxUrls);底下也是另一個笨邏輯的實際案例:
while(matcher.find()) {
String link = m.group(1).trim();
if(link.length() < 1) { continue; }
if(link.charAt(0) == '#') { continue; }
...還有三段類似的 if ... continue
基於篇幅限制,只有一行程式碼的區塊在上頭被列在一行,可以想像原程式會在視覺上造成的冗長程度,可以將相反邏輯的link.length() >= 1 && link.charAt(0) != '#'等條件整理至boolean isLinkWanted(String link),原程式就可以改為:
while(matcher.find()) {
String link = m.group(1).trim();
if(isLinkWanted(link)) {
...原本的五段if...continue後的流程
}
開始切割程式
當重新編排程式、削減笨邏輯的工作進行得差不多,程式碼某些區塊(通常是同一層區塊),就會呈現出緊湊、彼此相關、完成某項或某幾項工作的樣貌,試著將這些區塊導入新方法(函式)。
而有個好的重構工具是很有用的,這會自動為你建立新方法需要的參數與傳回值,試著為新方法取個適當的名稱,像是downloadAndSearch(URL url),那麼也許表示它含有兩個不同程度的抽象操作,還可以分解為download(URL url)與search(text, pageContent)兩個方法。
切割程式的過程中,對原方法來說,也是個大量消減程式碼的過程,有些區塊的細節簡化為一個具意義的方法名稱,原方法的邏輯會逐漸清晰,這時,就可以試著將重要的資訊逐步集中,例如:
Set<String> toCrawlSet = new LinkedHashSet<>();
checkOption();
toCrawlList.add(startUrl);
checkOption是個新導入的方法,裡頭提取了約20多行檢查建構物件時,傳入的選項是否符合要求的邏輯,可以想像原程式在宣告、建立物件並指定給toCrawSet之後,再經過約20多行才真正使用到toCrawlSet,實際上,checkOption所處的位置也不對,它應該放到建構式中,由於建立為新方法了,你只要在建構式中呼叫checkOption,而不是將原有20多行程式碼複製到建構式。
使用重構工具自動建立、導入新方法,有時會發現某些引數,會從第一個方法逐層傳遞至第n個方法,這類引數會將程式糾結在一起,試著將方法參數改為傳回值,切開這類引數造成的盤根錯節。
例如為了收集某些結果,將disallowList傳給processRobotsTxt,而processRobotsTxt又將disallowList傳給disallowListFrom,最終只是為了在disallowListFrom中使用disallowList的add方法,這時不如讓disallowListFrom傳回List<String>,processRobotsTxt改用disallowList.addAll(disallowListFrom(line)),進一步地,processRobotsTxt也可以去除參數改傳回List<String>,而呼叫processRobotTxt的方法,改為List<String> disallowList = processRobotsTxt(host)。
閱讀與理解既有程式碼的過程
削減笨邏輯與切割程式的動作,會是不斷地迭代進行,直到你能讀懂這些被切割出來的小方法為止。只有在這些小方法能被閱讀的情況下,才能瞭解其功能性,進一步決定如何改善它,或者是適當地將try-with-resources、lambda等新技術套用上去。
比較聰明的整合開發工具,也會給你這類提示,試著採用看看,會有什麼效果;也只有在這些小方法能被閱讀的情況下,才能察覺隱藏的Bug,或者是更多冗餘的東西。
當然,打開一個程式碼檔案,發現當中有二萬多行,確實會有種瞬間沉入屎坑的感覺,也會有視而不見、直接將想要的功能加入之衝動(也許就真的行動了),寫新的程式總是比理解既有程式來得簡單多了,只不過,多少遵守一下Bob大叔的〈童子軍規則〉:「在離開營地前,讓營地比使用前更加乾淨」,就算是整理小部份也好,否則,就算你寫的程式碼再好,倒入既有的屎坑,日後他人(或甚至自己)來看,仍舊只是一整坑攪和的屎罷了!