1. 程式人生 > >讀書報告之《修改程式碼的藝術》 (I)

讀書報告之《修改程式碼的藝術》 (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()操作,並把它新增到管理池中。

這樣描述後,要想到後面兩種方法就會更自然一些。
所以說需求描述是很關鍵。但是沒人會為我們做這個,一切只能靠自己。一切從需求分析開始。