轉載自 http://www.erptao.org
關於代碼重復最聞名的單詞是Kent Beck的Once And Only Once,也就是說軟件操作的任何一個片斷--不管是一個算法,一個常量集合,用於閱讀的文檔或者其他東西--應當只出現一次。
軟件重復出現至少會導致以下問題:
? 其中的一個版本會過期
? 代碼的責任會四處散開,導致代碼難以理解
? 當你修改代碼時,需要重復修改很多地方,一不小心就會遺漏
? 你不能很好地進行性能優化
我以前的一位老板曾經跟我炫耀過他手下編程的能力:“他只要把一份模版代碼拷過去,稍加修改,就可以完成一個新的模塊“。我驚奇這位程序員思路清楚的同時也懷疑這樣的程序除了他自己以外還有誰能維護,我想可能連他自己也無法做到。
重復代碼的產生由各種各樣的原因,上面的例子就是一個,我經常看到程序員把幾行或一整段代碼從這裡復制到這裡,然後少加修改,就變成了一份新的代碼。這裡的原因是程序員可以通過極少的努力就完成代碼重用,但是我們可以來看看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,然後大家都調用該method;
2. 兩個兄弟子類之間有相同的表達式,那麼在這兩個子類中使用Extract Method,接著使用pull up field,移到共同的超類
3. 假如結構相似而並非完全相同,用Extract method把相同部分和不同部分分開。然後使用Form Template method.
4. 假如方法使用不同的算法做相同的事情,那麼使用substitute algorithm
5. 假如在兩個不相干的類中有重復代碼,那麼在一個類中使用Extract class,然後在其他類中使用該class對象作為元素。
等等。
重復代碼需要refactoring是毫無疑問的,要害在於,你如何找到重復代碼,假如所有的重復代碼都是死板的重復,那問題是很輕易解決的。但是軟件開發的復雜因素可能往往使重復代碼表現為相似性而並非完全的重復。這些相似性可能並非一眼就能看出來。而是需要經過其它的Refactory步驟和一定的先見之明。
另一個問題就是排除重復代碼的粒度,只有大段的重復代碼有價值去排除,還是即使是小小的2、3句重復代碼就應該去排除。重復代碼排除的基本方法是建立自己單獨的方法,假如系統中許許多多的方法都很小,方法之間相互調用的開銷就會增加,它同時也增加了維護的開銷。
但是,這些開銷是值得的。方法是覆蓋的最小粒度,能夠被覆蓋的粒度越小,能夠重用的范圍和成都就愈廣。但在這個問題上也不要走極端,只有當一個方法實現一個具體的可以用Intent Revealing Name(揭示意圖的名字)命名時,一段代碼才值得稱為一個方法,而不是考慮其代碼的多少。
Martin Fowler在他的refactoring中描述了很多這樣的例子,Kent Beck則在Smalltalk Best Practice Pattern中更基礎地揭示了隱含在這些refactoing下的意圖。
下面是一個實際的例子,來自於Martin Fowler在ACM上的設計專欄:
class Invoice...
String asciiStatement() {
StringBuffer result = new StringBuffer();
result.append(“Bill for “ + customer + “
”);
Iterator it = items.iterator();
while(it.hasNext()) {
LineItem each = (LineItem) it.next();
result.append(“ ” + each.product() + “ ”
+ each.amount() + “
”);
}
result.append(“total owed:” + total + “
”);
return result.toString();
}
String HtmlStatement() {
StringBuffer result = new StringBuffer();
result.append(“
Bill for ” + customer + “
”);
result.append(“”);
Iterator it = items.iterator();
while(it.hasNext()) {
LineItem each = (LineItem) it.next();
result.append(“ ” + each.product()
+ “ ” + each.amount() + “
”);
}
result.append(“ ”);
result.append(“
total owed:” + total + “
”);
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 + “
”;
}
public String item(LineItem line) {
return “ ” + line.product()+ “ ” + line.amount() +“
”;
}
public String footer(Invoice iv) {
return “total owed:” + iv.total + “
”;
}
}
象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包含一個通用的結構,重復性已經被排除。更重要的是,你可以實現其它的Printer,XXXPrinter,從而能夠輕易地擴展系統。
BTW,Martin Fowler在這裡使用了Dispatched Interpretation模式,statement隱瞞了內部的細節,它隱藏內部的數據和表示,當它需要Printer做一件事情時,它負責解碼內部的數據結構,然後反過來把消息傳給Printer.
參見:Martin Fowler:Refactoring:Improve the design of Existing Code
Kent Beck : Smalltalk Best Pratice Pattern
ACM: Martin Fowler Design column:Reduce repetation
Kent Beck: Extreme Programming Explained