讀書報告之《修改程式碼的藝術》 (I)
《修改程式碼的藝術》,英文名《Working Effectively with Legacy Code》,中文翻譯的文筆上絕對談不上“藝術”二字,愧對藝術二字(當然譯者不是這個意思)。書中第三部分不論是例子還是解說都有點混亂,遠不如《重構——改善既有程式碼設計》一書。此書精華在於第一、二部分。
如何學習這本書,作為一個最底層的碼農,作為長期在別人程式碼上修修補補的苦逼二手貨開發人員,我只能給的建議就是:你可以將它看做是如何做定製功能的指導書——從某種意義上講,很多時候引入測試,實際就是新增一個叫做“測試”的定製功能。而且,這樣似乎也恰好印證了該書的中文名”修改程式碼的藝術”。
其他的,我不想談,也不懂。就這樣。
既然是要將這本書看做是如何做定製功能的指導書,那麼就先從本書第二部分“修改程式碼的技術”開始看。
1. 降低修改的風險
- 好的程式碼編輯工具。
哪些把二進位制資料也能玩得出神入化的大牛,我就不考慮了。對像我這樣的普通猿類,沒能吃上ALZ112,更別提ALZ113了,智商有限。只能用工具補拙了。吐槽一下我們公司竟然基本都在用source insight,這個工具中文編碼支援又不好,搜尋和補全命令不強,每次都只能呵呵。
- 單一目標的編輯。
這個在重構一書中也反覆強調了。這裡個人的體會是,老老實實的遵循吧,當習慣成自然了,進步的時機就到了。別以為學了高量和相對論,經典力學就能隨便玩了。
2. 需要修改大量相同的程式碼
對修改相同程式碼,我近乎偏執,原因也許就是源於下面兩句作者的話:
- 當你熱情地消除程式碼中的重複時,就會驚訝地發現,設計會自己浮現出來。
- 消除重複是錘鍊設計的強大手段,它不僅能讓設計變得更靈活,還能令程式碼修改更快更容易。
這裡補充一點的是相同的程式碼,不一定是完全相同的程式碼。有時出現完全相同的程式碼,只是因為一種巧合;很多時候,碰到最多的是結構和邏輯上相似的重複程式碼。
3. 時間緊迫,必須修改
不管時間是否緊迫,作為一種自我保護的本能,一般修改時,儘可能將修改的程式碼集中到單獨的類或者方法中,實現上儘可能的是類似一種開關性質的,可以簡單的enable or disable。
但是,如果時間充裕,應該在可測的情況下進行可能的重構,我自己的感受是有時這種自我保護的本能太過強烈,有些時候會有些畸形,這樣寫出來的程式碼也許是最安全的,但不是最優雅的。就像很多貪心演算法,不總是最優,但往往還夠得上"優"
這裡借用原書的例子討論
原始的程式碼,對列表entries中的每個物件,依次執行postDate()物件,然後新增的transactionBundle的管理池中。
public void postEntries(List<Entry> entries) { for (Iterator<Entry> it =entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); entry.postDate(); } transactionBundle.getListManager().add(entries); }
現在有個新的需求,需求描述是這樣的:(需求描述實際是很關鍵的,不同的描述方式會不自覺的影響程式設計師的實現方式)
entries列表中不是所有的物件都要執行postDate()和新增進transactionBundle的管理池中。只有還尚未在transactionBundle的管理池中的物件才需要執行postDate()操作,只有那些執行了postDate()的entry物件,才需要新增到transactionBundle的管理池中。
根據上面的需求描述,如果你是那99.9%的人,一般就會這樣實現:
public voidpostEntries(List<Entry> entries) { // 記錄哪些entries中哪些物件執行了postDate() List<Entry>entriesToAdd = newLinkedList<Entry>(); for (Iterator<Entry> it =entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); // 只有那些不在transactionBundle管理池中的entry物件才需要執行postDate() if (!transactionBundle.getListManager().hasEntry(entry)) { entry.postDate(); entriesToAdd.add(entry); } } // 將那些執行了postDate的entry物件新增到transactionBundle管理池中 transactionBundle.getListManager().add(entriesToAdd); }
無疑,這樣的修改非常具有侵入性,一旦出錯,很難定位是本身已有的缺陷還是改動造成的——只有在深入理解程式碼的改動邏輯之後才能分析錯誤原因。這個不好。
這個需求,本質上就是先找出那些還沒有在管理池中的entry物件,然後執行postDate()和add()操作。因此這裡實際可以應用“新生方法”手法,引入一個侵入性相當弱的修改。
public voidpostEntries(List<Entry> entries) { // 先剔除那些已經在transactionBundle管理池中的entry物件 List<Entry> entriesToAdd =uniqueEntries(entries); for (Iterator<Entry> it = entriesToAdd.iterator();it.hasNext(); ) { Entry entry = (Entry)it.next(); entry.postDate(); } transactionBundle.getListManager().add(entriesToAdd); }
// 剔除那些已經在transactionBundle管理池中的entry物件 private List<Entry> uniqueEntries(List<Entry> entries) { // return entries; //如果出現錯誤,可以直接return。 // 新生方法的好處就是程式碼隔離,可以快速定位是修改引入的問題還是原始程式碼本身就有的bug List<Entry> result = new LinkedList<Entry>(); for (Iterator<Entry> it = entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); if (!transactionBundle.getListManager().hasEntry(entry)) { result.add(entry); } } return result; }
當然也可以引入外覆方法的手法。
外覆方法的第一步總是重新命名原有方法和引入外覆方法,外覆方法名就是原有方法名。這一步基本不會錯。
//rename "postEntries(List<Entry> entries)" aspostEntriesDirectly private voidpostEntriesDirectly(List<Entry> entries) { for (Iterator<Entry> it =entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); entry.postDate(); } transactionBundle.getListManager().add(entries); } // new wrapper method use signature "public voidpostEntries(List<Entry> entries)" public voidpostEntries(List<Entry> entries) { postEntriesDirectly(entries); }
下一步,調整外覆方法的實現,這裡基本與新生方法相同
// new wrapper method use signature "public voidpostEntries(List<Entry> entries)" public voidpostEntries(List<Entry> entries) { // 先剔除那些已經在transactionBundle管理池中的entry物件 List<Entry>entriesToAdd = uniqueEntries(entries); postEntriesDirectly(entriesToAdd); }
如果習慣了思考使用弱侵入式的修改方式,後面兩種方式會自然而然的得到。外覆方法與新生方法的區別是外覆方法保留了原有方法(只是方法名做了修改)。
如果有需要,還可以新生類和外覆類。原理都差不多。最後囉嗦一下,如果一開始需求是這樣描述的:
對entries列表中的Entry物件,首先要檢查是否已經在管理池中。只有不存在時才執行postDate()操作,並把它新增到管理池中。
這樣描述後,要想到後面兩種方法就會更自然一些。
所以說需求描述是很關鍵。但是沒人會為我們做這個,一切只能靠自己。一切從需求分析開始。