2008年11月21日 星期五

極品程式碼--refactoring踢到鐵板(Part 2)

從事軟體這個行業,其中一個有趣的地方在於,程式碼要給機器(或compiler)看,也同時要給人看。我發現自己寫程式也是依照這樣的順序。迷糊健忘的我,永遠記不住任何一種語言的語法,常常用的library也記不住用法。幾乎都是靠IDE提醒、靠查閱文件,以及一些試誤的過程,直到讓電腦總算照我的意思走。接下來就要開始refactor,整理架構。把程式整理到看起來好讀又自然,再開發下一個功能。如果不在意程式碼好讀,功能一走通就繼續做下一個工作,過一兩天那支程式可能連自己都看不懂。

至於這次遇到的極品程式碼,為甚麼能發展到這麼複雜?歷代貢獻過的原作者們為甚麼能駕馭這樣的怪物,而我發呆幾天了就是看不懂?應該有什麼關鍵是我沒抓到的。(不肯承認自己特別笨。)


這個系統難refactor的原因,除了前一封信所提的風格問題以外,還有一些不利的因素。首先,這個程式是不穩定的,它的行為不一定都正確。而refactoring的意思是不改變程式碼的行為而只改變程式碼的架構。所以在refactor過程發現怪異的邏輯,都會起很多懷疑。要相信它的行為(或是說將錯就錯)還是要大膽改掉,常常是很耗心力的判斷。(因為改成正常的行為常常反而讓程式碼簡單很多。)另一個問題在於,這個功能牽涉到與其他廠商的系統複雜互動,很難測試。

話說我總算下定決心,找一個程式區塊硬著頭皮做extract method。Visual Studio的貼心工具幫我抽出來的method大概像這樣︰(省略參數的型態宣告了,不然會更長。看官知道意思就好,請不要太講究語法的正確性。)

string newUglyMethod(strArg1, out intArg2, ref datArg3, ref strArg4, txtArg5, out strArg6)

(實在很討厭匈牙利命名法!)一共有七八個參數。這些ref, out,純Java的族群可能不了解。out的意思是這個參數是用來接值用的,執行完method會給它適當的值。一般參數只輸入不輸出,out參數卻只輸出不輸入。而ref就更厲害了,這種參數又負責輸入又負責輸出。總之,這樣的method夠醜吧!來回看一看,一點都沒有比原來好懂。

不過,也因為抽出了這個奇醜無比的method,讓我忍不住去改善它。如果減少參數的個數,會讓它美觀一點吧!於是開始研究每個參數。strArg1雖然是外部傳進來,不過在前面只有做宣告
string strArg1 = "";
基本上可以移除,只要把宣告放在newUglyMethod中就好。intArg2只管輸出(out),我發現它在newUglyMethod中取得值的那一行蠻獨立的,可以移到method外。

接下來的ref感覺上會比較麻煩。之所以會extract出ref參數來,大多數是因為那個變數像(Part 1)所舉的例子strSysInfo一樣,具有多重身份。datArg3在newUglyMethod中有個獨立的用途,可以將它自參數列移除,改為newUglyMethod中local的變數。strArg4就比較複雜,在newUglyMethod中有兩個用途。我用find/replace的功能自某一行開始將它換個名稱,之後每改個用途就換個名稱,把一個變數變為好幾個變數。然後補上新變數的宣告讓compiler不要抱怨。先脫離多重身份的命運,留待後面再進一步refactor。

這樣一步一步的簡化,也改掉不少討厭的匈牙利命名。漸漸的這個method變成這樣
string newUglyMethod(arg1, arg2)
稱它為uglyMethod似乎不公平了,經過整形他長得端正多了。更重要的是,邏輯變清楚了!我個人refactor的過程,常常將名稱換了又換,總覺得取適當的名字是很關鍵的一件事。

newUglyMethod處理完以後,又多找幾個典型的程式碼區塊,extract method,然後簡化參數列。經過幾個回合以後,程式前後的變數糾結都不見了,整體的邏輯架構漸漸浮現出來。另外還挑出許多完全沒作用的垃圾程式碼以及變數,一個個刪掉。程式碼的行數也大大降低。最重要的是,了解整個程式在做什麼以後,接下來的refactor就更快更有把握了。

整個過程,感覺很像小說中虛竹亂下一手,殺死自己一大塊,卻解開了珍瓏棋局的劇情。out, ref這種C#怪怪的特性,沒想到還幫了大忙,因為容許先產生不完美的中間產物,再逐步改善。反而不知道如果是Java的話,我會怎麼處理這樣的程式。不過別誤會,我還是比較喜歡Java,而且Eclipse比Visual Studio實在好用太多了。

真實世界的程式,比Martin Fowler的書中範例要險惡許多。雖然這次的過程驚險又辛苦,但是感覺是令人成長的震撼教育。學到的經驗總結如下︰

1. 這是我第一次處理完全看不懂,又很龐大的程式。對於Refactoring這套方法的信心又堅定許多。以後更不會怕老舊程式碼了。
2. 滴水穿石,勿以善小而不為。再小的refactor動作都不會沒有價值。哪怕只是縮排,刪掉沒用的註解、rename、縮小一個變數的scope等等。累積多了,會有意想不到的效果。
3. 有的時候遇到瓶頸,必須做一點暫時顛覆程式美學的動作才能突破現況。
4. 很長的程式只要能分解成較小的區塊,就是很好的進步,為未來更優美的refactoring打下基礎。
---請期待part 3

沒有留言: