重複的程式碼(Duplicate Code)

作者:一楹

關於程式碼重複最著名的單詞是 Kent Beck Once And Only One,也就是說軟體操作的任何一個片斷--不管是一個演算法,一個常數集合,用於閱讀的文件或者其他東西--應當只出現一次。

軟體重複出現至少會導致以下問題

l          其中的一個版本會過期。

l          程式碼的責任會四處散開,導致程式碼難以理解。

l          當你修改程式碼時,需要重複修改很多地方,一不小心就會遺漏。

l          你不能很好地進行性能優化。

我以前的一位老闆曾經跟我誇耀過他手下編程的能力:「他只要把一份模版程式碼拷貝過去,稍加修改,就可以完成一個新的模組」。我驚訝這位程式員思路清晰的同時,也懷疑這樣的程序除了他自己以外還有誰能維護,我想可能連他自己也無法做到。

重複程式碼的產生來自各式各樣的原因,上面的例子就是一個,我經常看到程式員把幾行或一整段程式碼從這裡複製到那裡,然後稍加修改,就變成了一份新的程式碼。這裡的原因是程式員可以通過極少的努力就完成程式碼重複使用,但是我們可以來看看 DavidHooker 提出的 7 軟體開發原則:

1.           第一原則:存在的理由(Pattern: TheReason)
一個軟體系統存在的理由就是:為它的使用者提供價值。你所有的決定都取決於這一點。在指定一個系統需求,在寫下一段系統功能,在決定硬體平台和開發過程之前,問你自己一個問題,「這樣做會為系統增加價值嗎?」,如果答案是 yes,做如果是 No不做。這個原則是其他原則的原則。

2.         第二原則能簡單就簡單,愚蠢!)KISS (Pattern: KeepItSimple)
軟體設計不是一個輕描淡寫的過程。在做任何一個設計時,你必須考慮很多因素。所有設計應當盡可能簡單,但是不要再比這簡單了。這樣產生的系統才是可以理解和容易維護的。這並不是說很多由意義的特性,因為這種簡單性也要被拋棄。確實很多更優雅的設計往往更簡單,但簡單並不意味著 quick and dirty.”。事實上,簡單是通過許多思考和一次又一次的反覆修改才達到的。這些努力的彙報就是更容易維護,程式碼錯誤更少。(看看是否違反)

3.         第三原則:保持遠見(Pattern: MaintainTheVision)
清晰的遠見是一個軟體專案成功的基礎。. 沒有這樣的遠見,專案開發最後就變成天天為一個不好的設計做補丁。Brooks 說:

「概念的完整性是系統設計中最重要的問題。」

Stroustrup 也說

「有一個乾淨的內部結構識構建一個可理解、可辨識、可維護、可測試系統的基礎。」

Booch 則總結道:

「只有當你對系統的體系由一個清晰的感覺,才可能去發現通用的抽象和機制。開發這種通用性最終導致系統更簡單,因此更小,更可靠如果你不斷地複製、粘貼、修改程式碼,最終你將陷入一個大泥潭(the Big Mud),你永遠不可能對系統有一個清晰的認識。」

4.         第四原則:你製造的,別人會消費(Pattern: WhatYouProduceTheyConsume)
軟體系統不是在真空中使用的。其他人會使用、維護、文件化你的系統。這依賴於對你系統的理解。所以,你設計、實現的東西應當能夠讓別人理解。要記住,你寫的程式碼並非只給計算機看,你要時時記住,程式碼還要給人看。(Kent Beck)如果到處氾濫似是而非的程式碼,別人如何能夠辨別這些程式碼的相似和不同,如何去理解這些程式碼之間具有何種關係

5.         第五原則:對將來開放( Pattern BuildForTodayDesignForTomorrow)
一個成功的軟體有很長的生命期。你必須能夠使得軟體能夠適應這樣和那樣的變化。所以,一開始就不要軟體設計到死角上去。請總是問一下自己「如果這樣,那麼…?」這個問題,你要考慮到各種各樣的可能性,而不光是圖省事。複製,粘貼一下即可。

6.         第六原則:為重複使用做好計畫
軟體樣式是重複使用計畫的一種。不斷重複的程式碼顯然不是這樣的計畫。(See CommentsOnSix)

7.         第七原則:思考!
在採取任何動作之前首先做一個清晰的、完整的考慮,這樣才能產生更好的結果。如果你考慮了,但還是產生錯誤的結果,那麼這種努力也是值得的。在你學習或研究類似的問題時,更容易理解和掌握。


這些原則告訴我們輕鬆地複製、粘貼和修改程式碼不可能產生好的,也就是容易理解、維護、重複使用的程式碼。但請不要走極端

我一直認為,一個好的軟體系統是各種因素權衡的結果,也就是你如何把握一個度的問題。重複程式碼產生的另外一個主要原因就是做得太多,XP 有一個基本原則叫做 You Arent Gonna Need It,它是說「只實現你真正需要的東西,從來不去實現你預期需要的東西」。如果你去實現你現在認為將來需要的東西,不一定就是你以後真正需要的東西。你處於現在的環境中可能無法理解你要實現東西究竟是什麼樣子的。你會浪費大量的時間去構造這樣不知道是否必須的可能性。同時,當你真正實現的時候就可能產生重複程式碼。

Martin Fowler 在它的 Refactoring 一書中有很多用來處理程式碼重複,包括:

1.           同一類別的兩個方法中有相同的運算式使用 Extract method,然後大家都呼叫(call) method

2.         兩個兄弟子類別之間有相同的運算式,那麼在這兩個子類中使用 Extract Method接著使用 pull up field移到共同的超類別。

3.         如果結構相似而並非完全相同,用 Extract method 把相同部分和不同部分分開。然後使用 Form Template method

4.         如果方法使用不同的演算法做相同的事情,那麼使用 substitute algorithm

5.         如果在兩個不相干的類別中有重複程式碼,那麼在一個類別中使用 Extract class,然後在其他類別中使用該 class 物件作為元素。

等等。

重複程式碼需要 refactoring 是毫無疑問的,關鍵在於,你如何找到重複程式碼,如果所有的重複程式碼都是死板的重複,那問題是很容易解決的。但是軟體發展的複雜因素可能往往使重複程式碼表現為相似性而並非完全的重複。這些相似性可能並非一眼就能看出來。而是需要經過其它的 Refactory 步驟和一定的先見之明。

另一個問題就是排除重複程式碼的粒度,只有大段的重複程式碼有價值去排除,還是即使是小小的 23 句重複程式碼就應該去排除。重複程式碼排除的基本方法是建立自己單獨的方法,如果系統中許許多多的方法都很小,方法之間相互呼 叫的成本就會增加,它同時也增加了維護的成本

但是,付出這些成本是值得的。方法是覆蓋的最小粒度,能夠被覆蓋的粒度越小,能夠重複使用的範圍就愈廣。但在這個問題上也不要走極端,只有當一個方法實現一個具體的可以用 Intent Revealing Name(揭示意圖的名字)命名時,一段程式碼才值得稱為一個方法,而不是考慮其程式碼的多少。

Martin Fowler 在他的 refactoring 中描述了很多這樣的例子,Kent Beck 則在 Smalltalk Best Practice Pattern 中更基礎地揭示了隱含在這些 refactoring 下的意圖。

下面是一個實際的例子,來自於 Martin Fowler ACM 上的設計專欄:

class Invoice...

String asciiStatement() {

StringBuffer result = new StringBuffer();

result.append(“Bill for “ + customer + “\n”);

Iterator it = items.iterator();

while(it.hasNext()) {

LineItem each = (LineItem) it.next();

result.append(“\t” + each.product() + “\t\t”

+ each.amount() + “\n”);

}

result.append(“total owed:” + total + “\n”);

return result.toString();

}

String htmlStatement() {

StringBuffer result = new StringBuffer();

result.append(“<P>Bill for <I>” + customer + “</I></P>”);

result.append(“<table>”);

Iterator it = items.iterator();

while(it.hasNext()) {

LineItem each = (LineItem) it.next();

result.append(“<tr><td>” + each.product()

+ “</td><td>” + each.amount() + “</td></tr>”);

}

result.append(“</table>”);

result.append(“<P> total owed:<B>” + total + “</B></P>”);

return result.toString();

}

}

asciiStatement htmlStatement 具有類似的基礎結構,但是它們的實際步驟卻有所不同。他們都完成三件事情:

1.     列印發票抬頭

2.   運用圈結構去處理每一個項目,並且列印

3.   列印發票頁尾

這種結構的相似性,和明白其意圖後,馬上我們使用 composed method(也就是 Martin Fowler Extract method)

interface Printer {

String header(Invoice iv);

String item(LineItem line);

String footer(Invoice iv);

}

static class AsciiPrinter implements Printer {

public String header(Invoice iv) {

return “Bill for “ + iv.customer + “\n”;

}

public String item(LineItem line) {

return “\t” + line.product()+ “\t\t” + line.amount() +“\n”;

}

public String footer(Invoice iv) {

return “total owed:” + iv.total + “\n”;

}

}

html 則可以實現 htmlPrinter.

class Invoice...

public String statement(Printer pr) {

StringBuffer result = new StringBuffer();

result.append(pr.header(this));

Iterator it = items.iterator();

while(it.hasNext()) {

LineItem each = (LineItem) it.next();

result.append(pr.item(each));

}

result.append(pr.footer(this));

return result.toString();

}

class Invoice...

public String asciiStatement2() {

return statement (new AsciiPrinter());

}

現在,statement 包含一個通用的結構,重複性已經被排除。更重要的是,你可以實現其他的 PrinterXXXPrinter從而能夠輕易地擴展系統。

BTWMartin Fowler 在這裡使用了 Dispatched Interpretation 樣式,statement 隱瞞了內部的細節,它隱藏內部的資料和表示,當它需要 Printer 做一件事情時,它負責解碼內部的資料結構,然後反過來把消息傳給 Printer

參考文獻:

Martin FowlerRefactoring : Improve the design of Existing Code

Kent BeckSmalltalk Best Pratice Pattern

ACMMartin Fowler Design column : Reduce repetation

Kent BeckExtreme Programming Explained

 

關於作者:

一楹是一個專注於物件導向領域的系統設計師,目前的工作是在中國大陸一家 ERP 公司內任職 CTO。他住在浙江杭州,有一個兩個月大的女兒。