模式與XP

概述

  模式和極端編程(XP)都爲軟件設計、開發者提供了無法用金錢衡量的幫助。但是迄今爲止XP大量關注於重構(refactoring),而對模式隻字不提。在這篇文章中,我問“爲什麼”,並且最終描述出模式怎樣以XP的方式更好地實現、以及XP怎樣因爲包含對模式的使用而變得更好。

致謝

  非常感謝Kent Beck、Martin Fowler和Ward Cunningham,他們爲這篇文章提出了友善的評論。

  仍在所知不多的時候我們就開始了自己的程序設計生涯,生產出的軟件也反映出了我們的缺乏經驗:我們創建的代碼臃腫、錯誤百出、脆弱、難以維護、難以擴展。隨着時間的流逝,我們成爲了更好的軟件設計者:我們從技術作家、專家那裏學習,我們從自己的錯誤中學習。現在我們編寫具有高度靈活性的軟件,它適應廣泛而且堅固。當被請求編寫一個新的系統時,我們知道查明當前和將來的需求,這樣我們可以設計軟件來處理當前和將來的需要。

  在軟件開發生涯的這個階段,極端編程告訴我們,我們經常對軟件過分設計(over-engineer)了。我們從自己的錯誤中學到了太多,我們不希望重複這些錯誤,所以我們在系統生命週期的早期做了大量的努力來創造靈活而堅固的設計。不幸的是,我們沒有認識到:如果這個系統永遠不需要這個程度的靈活性和堅固性,那麼我們所有的工作就都沒有意義了。我們過分設計了。

  我也曾經過分設計過。說實話,與其他設計者坐在一間房間裏考慮如何設計軟件來適應許多當前和將來的需求,這的確是一種樂趣。我們把自己學到的所有東西——尤其是那些最好的經驗——應用在設計中。我們常常知道需求的列表會改變,但用戶或客戶總是改變需求。不過,我們認爲我們可以足夠聰明地設計軟件,使軟件足夠靈活,使它能應付所有的需求變化。

  今天,極端編程將告訴你這是多麼愚蠢的做法。XP說,我們必須讓設計自己顯現出來,而不是去預測設計將是什麼樣子。XP說,“做可能起作用的最簡單的事”,因爲“你將不再需要它”。另外,Kent Beck說:

  你需要在一個強調溝通、簡單、反饋和勇氣的價值系統中選擇最好的工作方法,這樣你才能勇敢的脫離過分設計。[Beck1 00]

  同意。但是,現在我必須提到我的朋友Norm Kerth。Norm在軟件開發領域有豐富的經驗和見識。一年以前我問他“對XP有什麼想法”。他說:

  我喜歡XP裏的每樣東西。我關心的是:還有什麼不在XP中。[Kerth 99]

  當時,我只認爲Norm是一個保守派。但現在我不能確定了。XP明顯缺少的就是使用模式的經驗。儘管一些XP的創始人幫助建設了模式社團,但沒有哪一個堅定清楚的說明模式如何適應XP。

  一開始,這還沒有讓我感到迷惑。但現在,我的確感到迷惑。

  我感到迷惑,因爲我在XP和模式上的經驗讓我相信:在XP的場景中模式會工作得更好;並且當XP包含模式時,XP也會工作得更好。

  這需要一些解釋。我將從描述我自己使用模式和XP的一些經驗開始。

  從1995年開始,我開始沉浸入模式之中。我學習模式文獻、主辦了一個每週一次的模式學習組、使用模式設計和開發軟件、並進行UP(一個關於使用模式的國際學術會議)的組織和運轉工作。說我“熱衷於模式”實在是一種保守的說法。

  當時,就象很多第一次學習模式的人一樣,我有一點過分渴望使用它們。這不是一件好事,因爲它會讓你的設計比需要的更復雜。但我沒有意識到這一點,直到我開始學習重構。

  大概在1996年,我第一次接觸到了重構。我開始實證它並很快觀察到重構帶我離開了我在模式學習中學到的某些原則。

  舉個例子,那本里程碑式的書——《設計模式:可複用面向對象軟件的基礎》——中的一個原則是:

  針對接口編程,而不是針對實現編程。[GHJV1 95]

  《設計模式》的作者們做了相當精彩的工作來解釋爲什麼我們需要遵循這條建議。幾乎在所有的模式中,都討論了當你針對某個特定實現編程時你的軟件如何變得缺少靈活性和可修改性。幾乎每一次都是接口過來幫忙。

  但如果我們不需要靈活性和可修改性,情況又是怎樣?爲什麼我們要在開始設計時預料一些可能永遠不會出現的需要?這是我的一次覺悟。所以隨後我記錄下了下面這個JAVA技巧:

不要分離類和接口

  我曾經習慣於在我的接口名字後面加上一個“I”。但當我繼續學習更多的重構技術時,我開始看到一種明智的做法:把類名和接口名設計成一樣。下面是原因:在開發過程中,你知道你可以使用一個接口來讓某些東西變得靈活(使實現多樣化),但可能你現在根本不需要讓實現多樣化。所以,放下預測太多的“過分設計”吧,你仍然保持簡單,仍然把東西放在一個類中。在某個地方你會寫一個方法語句來使用這個類的對象。然後,幾天、幾星期、幾個月之後,你明確“需要”一個接口。因此你就將原來的類轉換成一個接口,再創建一個實現類(實現新的接口),並且讓你原來的語句保持不變。[Kerievsky 96]

  我繼續學習類似於重構的課程,逐漸的,我使用模式的方式開始改變了。我不再預先考慮使用模式。現在,我更加明智了:如果某個模式可以解決某個設計問題,如果它提供一種方法來實現一個需求,我就會使用它,但我將從可以編碼出的模式的最簡單實現開始。晚些時候,當我需要增加或修改時,我將讓這個實現更加靈活、穩固。

  這種使用模式的新方法是一種更好的方法。它節約了我的時間,並讓我的設計更簡單。

  由於我繼續學到更多關於XP的知識,我很快開始考慮這樣一個事實:那些清楚介紹“XP是什麼”和“XP如何工作”的人毫不提及模式。看起來,焦點已經全部從開發轉向了重構。構造一點,測試一點,重構一點,然後再重複。

  那麼,模式怎麼了?

  我收到的一般的答案是:模式鼓勵過分設計,而重構保持事情簡單、輕量級。

  現在,我和其他任何人一樣喜歡重構——我回顧了Martin Fowler的書的關於這個主題的兩份手稿,然後知道重構將成爲一個標準。但我仍然喜歡模式,我發現模式在“幫助人們學會如何設計更好的軟件”方面是無價之寶。所以,XP怎麼能不包括模式呢?!

  我小心的在Portland Pattern Repository上寫下了我的不安。我問:是否完美的XP模式應該由完全不知道模式的程序員和指導者組成,是否他們應該完全依賴重構來“讓代碼去它該去的地方”。Ron Jeffries,世界上最有經驗的XP實踐者,與我爭論了這個主題,並且這樣寫:

  一個初學者不能傾聽代碼所說的話。他需要學習代碼質量的模式(在一般意義上)。他需要看好的代碼(以及,我猜,差的代碼),這樣他才能學會寫出好的代碼。

  一個問題——我的意思是一個可能的問題——是,現在的模式是否被用於幫助提高代碼的質量。我想Beck的Smalltalk Best Practice Patterns會有幫助,因爲那些都是非常小型的模式。我想設計模式都更值得懷疑,因爲模式和討論有時變得相當大,而且它們可能造成看起來合理的龐大解決方案。Martin Fowler的精彩的分析模式也有同樣的危險:在可以選擇一個小規模解決方案的時候選擇了大規模的解決方案。[Jeffries 99]

  一個非常有趣的關於模式的觀點。儘管我已經看到模式可以被明智的實現、使用,但Ron看起來卻認爲它們是危險的,因爲它們“讓龐大的解決方案看起來合理”。在其他地方,Ron觀察了一件經常發生的事情:第一次學習模式的人們如何過分渴望使用它們。

  我無法不同意後面這個觀察結果。就象任何新事物——甚至是XP——一樣,人們可能會過分渴望使用它們。但模式真的鼓勵在可以使用小規模解決方案時使用大規模解決方案嗎?

  我想這主要取決於你如何定義、使用模式。舉個例子,我觀察了許多模式的初級使用者,他們認爲一個模式與它的結構圖(或類圖)是完全相同的。只有在我向他們指出“模式可以根據需要以不同的方式實現”之後,他們纔開始發現這些圖只是表示實現模式的一種方式。

  模式的實現有簡單的也有複雜的。訣竅是:發現模式針對的問題,將這個問題與你當前的問題進行比較,然後將這個模式最簡單的實現(解決方案)與你的問題進行比較。當你這樣做時,你就不會在可以使用小規模解決方案的時候使用大規模解決方案。你獲得瞭解決問題最好的平衡。

  當人們沒有受過模式的良好訓練時,困難就可能出現。Ron提到人們使用模式的方式是“現在構成的”——這就是說,他們如何與現在的作者溝通。我同意模式文獻有一些缺點。關於模式的書很多,你可以花一些時間來理解模式解決的問題,這樣你就可以聰明的根據自己的特定需要選擇模式。

  這種選擇是極其重要的。如果你選擇了錯誤的模式,你可能過分設計或僅僅把你的設計揉在一起。有經驗的模式使用者也會犯錯誤,並且經常看到這樣的結果。但這些專家有其他的模式作爲裝備,這些模式可以幫助他們面對自己的錯誤。所以他們最終經常把自己真正需要的模式換成了不那麼理想的模式。

  那麼,你將怎樣成爲一個有經驗的模式使用者呢?我發現除非人們投身於大量模式的學習中,否則他們就有可能陷入誤解它們、過分使用它們以及用它們過分設計的危險之中。

  但這是避免使用模式的一個原因嗎?

  我想,不。我發現模式在如此多的項目中如此有用,以至於我無法想象不使用它們來進行軟件設計和開發。我相信對模式的徹底的學習是非常值得的。

  那麼,XP對模式保持沉默是因爲感覺到它們將被誤用嗎?

  如果情況是這樣,也許問題已經變成:我們怎樣使用模式中的智慧,而避免模式在XP開發場景中的誤用呢?

  在這裏,我想我必須回到《設計模式》。在“結論”一章、“設計模式將帶來什麼”一節、“重構的目標”小節中,作者寫道:

  我們的設計模式記錄了許多重構產生的設計結構。在設計初期使用這些模式可以防止以後的重構。不過即使是在系統建成之後才瞭解如何使用這些模式,它們仍可以教你如何修改你的系統。設計模式爲你的重構提供了目標。[GHJV2 95]

  這就是我們需要的觀點:重構的目標。這就是重構和模式之間的橋樑。它完美的描述了我自己在如何使用模式方面的進步:從簡單開始,考慮模式但將它們保持在次要地位,小規模重構,只有在真正需要模式的時候才把重構轉移爲模式。

  這個需要訓練和仔細判斷的過程將很好的適應XP所包含的最好的習慣。

  而且這個途徑很明顯與“故意不知道或不使用模式而只依賴重構來改善設計”的方法非常不同。

  只依賴重構的危險是:沒有目標,人們可能使設計小小進步,但他們的全面設計將最終受損害,因爲這種方法缺乏順序、簡單性和效力,而聰明的使用模式則可以讓開發者擁有這些。

  引用Kent Beck自己的話:模式生成體系結構。[Beck2 94]

  但模式不保證有紀律的使用。如果我們在設計中過多、過早的使用它們,我們就又回到了過分設計的問題。因此,我們必須回答這個問題:“在設計的生命週期中,何時引入模式是安全的?”請回憶上面對《設計模式》的引用:

  在設計初期使用這些模式可以防止以後的重構。

  這是一個聰明的主張。如果我們不知道“何時配置一個模式”的基本規則,那麼我們就很容易在設計週期的早期就陷入過分設計。

  再一次,問題又全部集中在一起:如何將項目中的問題與一個合適的模式相匹配。

  在這裏,我必須講述我爲不同行業開發軟件得到的經驗。

  有一家客戶要求我和我的團隊用JAVA爲他們的網站構造軟件,這將是一個很酷的交互式版本。這個客戶沒有任何JAVA程序員,但仍然要求能在他們需要的任何時候、任何地方修改軟件的行爲,而不必做程序的修改。多麼高的要求!

  在對他們的需要做了一些分析之後,我們發現Command模式將在這個設計中扮演一個非常重要的角色。我們將編寫命令對象,並讓這些命令對象控制軟件的整個行爲。用戶將可以參數化這些命令、將它們排序、並選擇命令運行的時間和地點。

  這個解決方案工作得很完美,Command模式正是成功的關鍵。所以在這裏,我們不會等到重構的時候才使用Command模式。相反,我們預先看到了使用它的需要,並從一開始就用Command模式來設計軟件。

  在另一個項目中,系統需要作爲獨立應用程序和WEB應用程序運行。Builder模式在這個系統中發揮了巨大的作用。如果沒有它,我不敢想象我們會拼湊出一個多麼臃腫的設計。Builder模式的作用就是解決“多平臺、多環境運行”這樣的問題。所以在設計早期就選擇它是正確的。

  現在,我必須聲明:即使在設計的早期引入了模式,但一開始仍然應該按照它們最原始的樣子來實現它們。只有在晚些時候,當需要附加的功能時,模式的實現才能被替換或升級。

  一個例子會讓你更清楚這一點。

  上面提到的由命令對象控制的軟件是用多線程的代碼實現的。有時候兩個線程會使用同一個宏命令來運行一系列命令。但一開始我們並沒有被宏命令的線程安全問題困擾。所以,當我們開始遇到線程安全造成的莫名其妙的問題時,我們必須重新考慮我們的實現。問題是,我們應該花時間構造宏命令的線程安全嗎?或者有沒有更簡單的方法來解決這個問題?

  我們用更簡單的方法解決了這個問題,並且避免了過分設計:爲每個線程提供一個獨立的宏命令實例。我們可以在30秒內實現這個解決方案。請把這個時間與設計一個線程安全的宏命令所需的時間做一下比較。

  這個例子描述了XP的哲學怎樣在使用模式的情況下保持事情簡單。沒有這種簡單化的驅動,過分設計的解決方案——就象線程安全的宏命令——很容易出現。

  因此,簡單化和模式之間的關聯是很重要的。

  當程序員需要做出設計決策時,很重要的一件事就是:他們應該試圖保持設計簡單,因爲簡單的設計通常比龐大而複雜的設計更容易維護和擴展。我們已經知道,重構意味着將我們保持在簡單的路上:它鼓勵我們以小而簡單步驟逐漸改進我們的設計,並避免過分設計。

  但是模式呢?難道它們不是幫助我們保持簡單嗎?

  有些人會說“不”。他們認爲模式儘管有用,但容易造成複雜的設計。他們認爲模式會造成對象快速增加,並導致過分依賴對象組合。

  這種觀點是由於對使用模式的方法的錯誤理解。有經驗的模式使用者會避免複雜的設計、對象的快速增長和過多的對象組合。

  實際上,在使用模式的時候,有經驗的模式使用者會讓他們的設計更簡單。我將再用一個例子來說明我的觀點。

  JUnit是一個簡單而有用的JAVA測試框架,它的作者是Kent Beck和Erich Gamma。這是一個精彩的軟件,其中滿是精心選擇的簡單的模式。

  最近一些人要求我對JUnit進行DeGoF,也就是說,將JUnit中的設計模式移除掉,以觀察沒有模式的JUnit是什麼樣子。這是一次非常有趣的練習,因爲它讓參與者認真考慮應該在什麼時候在系統中引入模式。

  爲了描述他們學到的東西,我們將對JUnit 2.1版中的一些擴展進行DeGoF。

  JUnit中有一個叫做TestCase的抽象類,所有的具體測試類都派生自它。TestCase類沒有提供任何多次運行的方法,也沒有提供在自己的線程中運行測試的方法。Erich和Kent用Decorator模式很優雅的實現了可重複測試和基於線程的測試。但是如果設計團隊不知道Decorator模式呢?讓我們看看他們會開發出什麼,並評估一下他們的設計有多簡單。

  這是Test Case在JUnit框架1.0版本中的樣子(爲了簡化,我們忽略了註釋和很多方法):

public abstract class TestCase implements Test {
private String fName;
public TestCase(String name) {

fName= name;

}

public void run(TestResult result) {

result.startTest(this);

setUp();

try {

runTest();

}

catch (AssertionFailedError e) {

result.addFailure(this, e);

}

catch (Throwable e) {

result.addError(this, e);

}

tearDown();

result.endTest(this);

}

public TestResult run() {

TestResult result= defaultResult();

run(result);

return result;

}

protected void runTest() throws Throwable {

Method runMethod= null;

try {

runMethod= getClass().getMethod(fName, new Class[0]);

} catch (NoSuchMethodException e) {

e.fillInStackTrace();

throw e;

}

try {

runMethod.invoke(this, new Class[0]);

}

catch (InvocationTargetException e) {

e.fillInStackTrace();

throw e.getTargetException();

}

catch (IllegalAccessException e) {

e.fillInStackTrace();

throw e;

}

}

public int countTestCases() {

return 1;

}

}

  新的需求要求允許測試重複進行、或在它們各自的線程中進行、或以上兩者。

  沒有經驗的程序員通常在遇到這樣的新需求時進行子類型化。但是在這裏,因爲知道TestCase對象將需要能夠在同一個線程中重複運行、或在各自獨立的線程中重複運行,所以程序員知道:他們需要考慮得更多。

  一種實現方法是:將所有的功能都添加給TestCase本身。許多開發者——尤其是那些不瞭解設計模式的開發者——將會這樣做,而不考慮這會使他們的類變得臃腫。他們必須添加功能,所以他們將功能添加到任何可以添加的地方。下面的代碼可能就是他們的實現:

public abstract class TestCase implements Test {
private String fName;
private int fRepeatTimes;
public TestCase(String name) {
this(name, 0);
}
public TestCase(String name, int repeatTimes) {
fName = name;
fRepeatTimes = repeatTimes;
}
public void run(TestResult result) {
for (int i=0; i < fRepeatTimes; i++) {
result.startTest(this);
setUp();
try {
runTest();
}
catch (AssertionFailedError e) {
result.addFailure(this, e);
}
catch (Throwable e) {
result.addError(this, e);
}
tearDown();
result.endTest(this);
}
}
public int countTestCases() {
return fRepeatTimes;
}
}

  請注意run(TestResult result)方法變大了一些。他們還爲TestCase類添加了另外的構造子。到目前爲止,這還不算什麼大事。並且,我們可以說:如果這就是所有必須做的事情,那麼使用Decorator模式就是多餘的。

  現在,如果要讓每個TestCase對象在其自己的線程中運行又怎樣呢?這裏也有一個可能的實現:

public abstract class TestCase implements Test {
private String fName;
private int fRepeatTimes;
private boolean fThreaded;
public TestCase(String name) {
this(name, 0, false);
}
public TestCase(String name, int repeatTimes) {
this(name, repeatTimes, false);
}
public TestCase(String name, int repeatTimes, boolean threaded) {
fName = name;
fRepeatTimes = repeatTimes;
fThreaded = threaded;
}
public void run(TestResult result) {
if (fThreaded) {
final TestResult finalResult= result;
final Test thisTest = this;
Thread t= new Thread() {
public void run() {
for (int i=0; i < fRepeatTimes; i++) {
finalResult.startTest(thisTest);
setUp();
try {
runTest();
}
catch (AssertionFailedError e) {
finalResult.addFailure(thisTest, e);
}
catch (Throwable e) {
finalResult.addError(thisTest, e);
}
tearDown();
finalResult.endTest(thisTest);
}
}
};
t.start();
result = finalResult;
} else {
for (int i=0; i < fRepeatTimes; i++) {
result.startTest(this);
setUp();
try {
runTest();
}
catch (AssertionFailedError e) {
result.addFailure(this, e);
}
catch (Throwable e) {
result.addError(this, e);
}
tearDown();
result.endTest(this);
}
}
}
public int countTestCases() {
return fRepeatTimes;
}
}

  唔,這看起來開始變得更壞了。爲了支持兩個新的特徵,我們現在擁有了三個構造子,而且run(TestResult result)方法的大小迅速的膨脹起來。

  即使不管所有這些新代碼,我們這些程序員還沒有滿足這些需求:我們仍然不能在各自的線程中重複運行測試。爲了這個目的,我們必須添加更多的代碼。算了,我就放過你吧。

  重構可以幫助這些代碼減小尺寸。但是隻需要稍做思考:如果再接到一個新的需求,我們要怎麼辦?現在JUnit 3.1支持四種不同的TestCase修飾器,它們可以輕鬆的隨意組合以獲取所需的功能。同時,JUnit的實現仍然簡單——沒有混亂的代碼。這種設計保持TestCase類的簡單、輕量級,用戶只需要在需要的時候對TestCase對象進行裝飾即可,而且可以選擇任何組合順序。

  很清楚,這是一個模式幫助簡化設計的例子。這個例子也說明了缺乏經驗的開發者怎樣改善他們的設計——如果他們知道模式指出的重構目標。

  使用模式來開發軟件是聰明之舉,但如果你缺乏使用模式的經驗,它也可能是危險的。出於這個原因,我極力提倡模式學習組。這些學習組讓人們在同伴的幫助下穩步前進而精通模式。

  當人們瞭解模式並以受過訓練的方式使用它們時,模式是最有用的——這種受過訓練的方式就是XP的方式。以XP的方式使用模式鼓勵開發者保持設計的簡單、並完全根據需要對模式進行重構。它鼓勵在設計早期使用關鍵的模式。它鼓勵將問題與能幫助解決問題的模式相匹配。最後,它鼓勵開發者編寫模式的簡單實現,然後根據需要發展它們。

  在XP的場景中,模式的確更有用;而在包含對模式的使用時,XP開發則更有可能成功。

參考書目

[Beck1 00] Beck, Kent. Email on [email protected], January 2000.

[Beck2 94] Patterns Generate Architectures, Kent Beck and Ralph Johnson, ECOOP 94

[GHJV1 95] Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. 中譯本:《設計模式:可複用面向對象軟件的基礎》,李英軍等譯。

[GHJV2 95] Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. Pages 353-354 中譯本:《設計模式:可複用面向對象軟件的基礎》,李英軍等譯,第6章。

[Jeffries 99] Jeffries, Ron. Patterns And Extreme Programming. Portland Pattern Repository. December, 1999

[Kerth 99] Kerth, Norm. Conversation, circa March, 1999.

[Kerievsky 96] Kerievsky, Joshua. Don’t Distinguish Between Classes And Interfaces. Portland Pattern Repository. Circa 1996

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章