模式重構(Pattern refactoring)
這一章我們會專注于通過逐漸演化的方式應用設計模式來解決問題。也就是說,一開始我們會用比較粗糙的設計作為最初的解決方案,然後檢驗這個解決方案,進而針對這個問題使用不同的設計模式(有些模式是可行的,有些是不合适的)。在尋找更好的解決方案的過程中,最關鍵的問題是,“哪些東西是變化的?”
這個過程有點像Martin Fowler在《重構:改善既有代碼的設計》那本書裡談到的那樣(盡管他是通過代碼片斷而不是模式級别的設計來讨論重構)。以某個解決方案作為開始,當你發現這個解決方案不能再滿足你的需要的時候就修正它。當然,這是一種很自然的做法,但是對于過程式的計算機程式設計來說要完成它是相當困難的,大家對于代碼重構和設計重構的接受更加說明了面向對象程式設計是個“好東西”。
模拟一個廢品循環再生器
這個問題的本質是這樣的,廢品在未分類的情況下被扔進垃圾箱,這樣特定的類别資訊就丢失了。但是,到後面為了給這些廢品正确的分類,那些特定的類别資訊又得被恢複出來。一開始,我們采用RTTI(Thinking in Java第二版第12章有述)作為解決方案。
這并非是一個輕而易舉就能完成的設計,因為它有一些額外的限制。也正是因為有了這些限制才是這個問題更加有趣——它更像你在工作中可能會碰到的那些棘手的問題。額外的限制是指,這些垃圾運到廢品再生廠(trash recycling plant)的時候,它們是混合在一起的。我們的程式必須要模拟廢品分類。這正是需要RTTI的地方,你有一大堆叫不出名字的廢品碎片,而我們的程式需要找出它們的确切類型。
//: refactor:recyclea:RecycleA.java
// Recycling with RTTI.
package refactor.recyclea;
import java.util.*;
import java.io.*;
import junit.framework.*;
abstract class Trash {
private double weight;
Trash(double wt) { weight = wt; }
abstract double getValue();
double getWeight() { return weight; }
// Sums the value of Trash in a bin:
static void sumValue(Iterator it) {
double val = 0.0f;
while(it.hasNext()) {
// One kind of RTTI:
// A dynamically-checked cast
Trash t = (Trash)it.next();
// Polymorphism in action:
val += t.getWeight() * t.getValue();
System.out.println(
"weight of " +
// Using RTTI to get type
// information about the class:
t.getClass().getName() +
" = " + t.getWeight());
}
System.out.println("Total value = " + val);
}
}
class Aluminum extends Trash {
static double val = 1.67f;
Aluminum(double wt) { super(wt); }
double getValue() { return val; }
static void setValue(double newval) {
val = newval;
}
}
class Paper extends Trash {
static double val = 0.10f;
Paper(double wt) { super(wt); }
double getValue() { return val; }
static void setValue(double newval) {
val = newval;
}
}
class Glass extends Trash {
static double val = 0.23f;
Glass(double wt) { super(wt); }
double getValue() { return val; }
static void setValue(double newval) {
val = newval;
}
}
public class RecycleA extends TestCase {
Collection
bin = new ArrayList(),
glassBin = new ArrayList(),
paperBin = new ArrayList(),
alBin = new ArrayList();
private static Random rand = new Random();
public RecycleA() {
// Fill up the Trash bin:
for(int i = 0; i < 30; i++)
switch(rand.nextInt(3)) {
case 0 :
bin.add(new
Aluminum(rand.nextDouble() * 100));
break;
case 1 :
bin.add(new
Paper(rand.nextDouble() * 100));
break;
case 2 :
bin.add(new
Glass(rand.nextDouble() * 100));
}
}
public void test() {
Iterator sorter = bin.iterator();
// Sort the Trash:
while(sorter.hasNext()) {
Object t = sorter.next();
// RTTI to show class membership:
if(t instanceof Aluminum)
alBin.add(t);
if(t instanceof Paper)
paperBin.add(t);
if(t instanceof Glass)
glassBin.add(t);
}
Trash.sumValue(alBin.iterator());
Trash.sumValue(paperBin.iterator());
Trash.sumValue(glassBin.iterator());
Trash.sumValue(bin.iterator());
}
public static void main(String args[]) {
junit.textui.TestRunner.run(RecycleA.class);
}
} ///:~
在與本書配套的源代碼清單上,上面那個檔案位于recyclea子目錄裡,而recyclea又是refactor子目錄的一個分支。拆包(unpacking)工具會把它放到适當的子目錄裡。這麼做的理由是,本章把這個特定的例子重寫了好多次,把每個版本放到它們自己的目錄裡(通過使用每個目錄的預設package,程式調用也很簡單)可以避免類名字沖突。
程式建立了幾個ArrayList對象用來存放Trash對象的引用。當然,ArrayLists實際上存放的是Objects對象,這樣它們就可以存放任何東西。它們之是以存放Trash對象(或者由Trash派生出來的對象)隻不過是因為你的小心翼翼,你不把Trash以外的東西傳給它們。如果你往ArrayList裡存放了“錯誤”的東西,你不會在編譯時刻得到警告或者錯誤——你隻能在運作時刻通過異常發現這些錯誤。
當Trash對象的引用添加到ArrayList以後,它們就丢失了特定的類别資訊,變為隻是Object對象的引用(它們被upcast了)。但是,由于多态的存在,當通過Iterator sorter調用動态綁定的方法時它還是會産生合适的行為,一旦最終的Object對象被cast回Trash對象,Trash. sumValue( )也采用一個Iterator來完成針對ArrayList中每個對象的操作。
先把不同類型的Trash對象upcast并放到一個能夠存放基類引用的容器裡,然後再把它們downcast出來,這麼做看起來很傻。為什麼不在一開始直接把Trash對象放到适當的容器裡? (事實上,這就是垃圾回收這個例子令人迷惑的地方)。對于上面的程式要這麼改動是很容易的,但是有時候采用downcasting這種方法對于某些系統的結構和靈活性都是很有好處的。
上面的程式滿足了設計要求:它能夠工作。如果隻要求一個一次性的解決方案,那上面的方法就可以了。但是實用的程式通常是需要随着時間演化的,是以你必須得問問,“如果情況改變了會怎麼樣呢?”比如,現在硬紙闆成了有用的可循環利用的物品,那該怎麼把它內建到上面的系統呢(尤其是當程式又大又複雜的時候)。因為上面的例子裡Switch語句裡那些類型檢查的代碼是分散在整個程式裡的,每次添加新類型的時候你就得查找所有類型檢查的代碼,如果你漏掉一個編譯器是不會通過報告錯誤的方式給你提供任何幫助的。
這裡針對每一種類型都進行測試,其實是對RTTI的一種誤用。如果你隻是因為某種子類型需要特殊對待而對其進行測試,那可能是恰當的。但是如果你是在針對Switch語句的每一個類型都進行測試,那麼你可能是錯過了某些重要的東西,而且這将注定使你的代碼更加難以維護。下一小節,我們會看看這一程式是如何通過幾個階段的演化變得更加靈活的。對于程式設計,這是一個有價值的例子。
改進現有設計Improving the design
《設計模式》一書中,是圍繞着“随着程式不斷演化哪些東西将會發生變化?”這個問題來組織解決方案的。這對于任何設計來說通常都是最重要的一個問題。如果你能夠圍繞這個問題的答案來建構你的系統,将會帶來一舉兩得的好處:不僅僅是你的系統容易維護(而且廉價),而且你還很可能創造出可以重用的元件(components),這樣别的系統就更容易建構。這是面向對象程式設計本來就有的好處,但它不會自動發生;它需要你對于問題的思考和洞察力。這一小節我們來看看在完善系統的過程中它是怎麼發生的。
對于我們的廢品回收系統來說,“什麼是變化的?”這個問題的答案是非常普通的:更多類型的(廢品)會被加入到系統中來。也就是說,這個設計的最終目标是使得添加新的類型盡可能的友善。對于廢品回收程式,我們想要做的是把所有涉及到特定類型資訊的地方都封裝起來,這樣(如果沒有别的原因)任何改動都可以被放到那些封裝好的地方。最後的結果是這個過程也在相當程度上使程式其餘部分的代碼變得整潔。
多弄些對象“Make more objects”
這将引出一條常用的面向對象設計原則,我第一次是從Grady Booch那裡聽到的:“如果你的設計過于複雜,那就多弄些對象。”這條原則不但違反直覺而且簡單的近乎荒謬,但它卻是我所見過的最有用的指導原則。(你可能已經覺察到“多弄些對象”經常等同于“添加另外一個中間層。”)通常來說,如果你發現哪個地方代碼非常淩亂,就可以考慮加入什麼樣的類可以把它弄的整潔一些。整理代碼經常會帶來另外一個好處是系統會擁有更好的結構和靈活性。
Trash對象最初是在main()函數的switch語句裡被建立的,
for(int i = 0; i < 30; i++)
switch((int)(rand.nextInt(3)) {
case 0 :
bin.add(new
Aluminum(rand.nextDouble() * 100));
break;
case 1 :
bin.add(new
Paper(rand.nextDouble() * 100));
break;
case 2 :
bin.add(new
Glass(rand.nextDouble() * 100));
}
毫無疑問,上面的代碼顯的有些淩亂,而且當加入新的類型的時候你必須得改變這段代碼。如果經常需要添加新類型,比較好的解決辦法是使用一個單獨的方法(method),這個方法利用所有必需的資訊産生一個針對某一合适類型的對象的引用,這個引用會先被upcast成一個trash對象。《設計模式》一書提到這種方法的時候籠統的把它叫做建立型模式(creational pattern )(實際上有好幾種建立型模式)。這裡将要用到的特定模式是工廠方法(Factory Method)的一個變種。這裡,工廠方法是Trash的一個靜态成員函數,而更多的情況下它是作為一個被派生類覆寫的方法而存在的。
for(int i = 0; i < 30; i++)
switch((int)(rand.nextInt(3)) {
case 0 :
bin.add(new
Aluminum(rand.nextDouble() * 100));
break;
case 1 :
bin.add(new
Paper(rand.nextDouble() * 100));
break;
case 2 :
bin.add(new
Glass(rand.nextDouble() * 100));
}
毫無疑問,上面的代碼顯的有些淩亂,而且當加入新的類型的時候你必須得改變這段代碼。如果經常需要添加新類型,比較好的解決辦法是使用一個單獨的方法(method),這個方法利用所有必需的資訊産生一個針對某一合适類型的對象的引用,這個引用會先被upcast成一個trash對象。《設計模式》一書提到這種方法的時候籠統的把它叫做建立型模式(creational pattern )(實際上有好幾種建立型模式)。這裡将要用到的特定模式是工廠方法(Factory Method)的一個變種。這裡,工廠方法是Trash的一個靜态成員函數,而更多的情況下它是作為一個被派生類覆寫的方法而存在的。
for(int i = 0; i < 30; i++)
switch((int)(rand.nextInt(3)) {
case 0 :
bin.add(new
Aluminum(rand.nextDouble() * 100));
break;
case 1 :
bin.add(new
Paper(rand.nextDouble() * 100));
break;
case 2 :
bin.add(new
Glass(rand.nextDouble() * 100));
}
毫無疑問,上面的代碼顯的有些淩亂,而且當加入新的類型的時候你必須得改變這段代碼。如果經常需要添加新類型,比較好的解決辦法是使用一個單獨的方法(method),這個方法利用所有必需的資訊産生一個針對某一合适類型的對象的引用,這個引用會先被upcast成一個trash對象。《設計模式》一書提到這種方法的時候籠統的把它叫做建立型模式(creational pattern )(實際上有好幾種建立型模式)。這裡将要用到的特定模式是工廠方法(Factory Method)的一個變種。這裡,工廠方法是Trash的一個靜态成員函數,而更多的情況下它是作為一個被派生類覆寫的方法而存在的。
for(int i = 0; i < 30; i++)
switch((int)(rand.nextInt(3)) {
case 0 :
bin.add(new
Aluminum(rand.nextDouble() * 100));
break;
case 1 :
bin.add(new
Paper(rand.nextDouble() * 100));
break;
case 2 :
bin.add(new
Glass(rand.nextDouble() * 100));
}
毫無疑問,上面的代碼顯的有些淩亂,而且當加入新的類型的時候你必須得改變這段代碼。如果經常需要添加新類型,比較好的解決辦法是使用一個單獨的方法(method),這個方法利用所有必需的資訊産生一個針對某一合适類型的對象的引用,這個引用會先被upcast成一個trash對象。《設計模式》一書提到這種方法的時候籠統的把它叫做建立型模式(creational pattern )(實際上有好幾種建立型模式)。這裡将要用到的特定模式是工廠方法(Factory Method)的一個變種。這裡,工廠方法是Trash的一個靜态成員函數,而更多的情況下它是作為一個被派生類覆寫的方法而存在的。
factory method模式的思想是這樣的,你把建立對象所需要的關鍵資訊傳遞給它,然後它會把(已經upcast成基類的)引用作為傳回值傳給你。然後,你就可以利用這個對象的多态性了。這麼一來,你甚至再也不需要知道被建立對象的确切類型。實際上,factory method為了防止你意外的誤用所建立的對象,而把它的類型資訊隐藏起來了。如果你想在不使用多态的情況下操縱對象,就必須得顯式的使用RTTI和casting。
但是會有一些小問題,尤其是當你使用更為複雜的方法(這裡沒有列出),在基類裡定義factory method而在派生類裡覆寫它的時候。
如果(建立)派生類所需的資訊需要(比基類)更多的或者是不同的參數,那該怎麼辦呢?
“建立更多的對象”就可以解決這個問題。為了實作factory method模式,Trash類添加了一個新的叫factory的方法。為了隐藏建立對象所需的資料,新加了一個Messenger類,它攜帶了factory方法建立合适的Trash對象所必需的所有資訊(本書開始的時候我們把Messenger也稱作一個設計模式,但是它确實太簡單了,可能你不想把它提升到這麼高的高度)。下面是Messenger的一個簡單實作:
class Messenger {
int type;
// Must change this to add another type:
static final int MAX_NUM = 4;
double data;
Messenger(int typeNum, double val) {
type = typeNum % MAX_NUM;
data = val;
}
}
Messenger對象的唯一任務就是為factory()方法儲存它所需的資訊。現在,如果某種情況下factory方法為了建立某一新類型的Trash對象需要更多的或者不同的資訊,factory()接口就沒必要改變了。Messenger類可以通過添加新的資料和新的構造函數來改變,或者采用更典型的面向對象的方法——subclassing。
這個簡單例子裡的factory()方法看起來像下面的樣子:
static Trash factory(Messenger i) {
switch(i.type) {
default: // To quiet the compiler
case 0:
return new Aluminum(i.data);
case 1:
return new Paper(i.data);
case 2:
return new Glass(i.data);
// Two lines here:
case 3:
return new Cardboard(i.data);
}
}
這裡,可以很簡單的決定對象的确切類型,但你可以想象一下更為複雜的系統,在那個系統裡factory()方法使用複雜難懂的算法。關鍵問題是這些東西現在都被隐藏到了同一個地方,當添加新類型的時候,你很清楚該到這裡來改。
現在,建立新對象要比在main()函數裡簡單多了
for(int i = 0; i < 30; i++)
bin.add(
Trash.factory(
new Messenger(
rand.nextInt(Messenger.MAX_NUM),
rand.nextDouble() * 100)));
建立Messenger對象是為了用它傳遞資料給factory()方法,然後factory()方法會在堆上(heap)建立某一類型的Trash對象并returns the reference that’s added to the ArrayList bin.
當然,如果你改變了參數的個數和類型,上面的代碼還需要改動,但是如果Messenger對象是自動生成的那就可以避免這樣的改動了。例如,可以用一個包含所需參數的ArrayList傳給Messenger對象的構造函數(或者直接傳給factory()方法也可以)。這麼做需要在運作時刻解析和檢驗傳入的參數,但它的确提供了最大的靈活性。
從這段代碼你可以看出factory是負責解決哪一類“一系列變化”的問題的:如果你向系統添加新的類型(所謂變化),必需要改變的隻是factory内部的代碼,也就是說factory把這部分變化所帶來的影響隔離出來了。
to be continued......
目錄