接下來,讓我們思考如何將具有完全不同目標的一個設計范式應用到垃圾歸類系統。
對這個范式,我們不再關心在系統中加入新型Trash時的優化。事實上,這個范式使新型Trash的添加顯得更加復雜。假定我們有一個基本類結構,它是固定不變的;它或許來自另一個開發者或公司,我們無權對那個結構進行任何修改。然而,我們又希望在那個結構裡加入新的多形性方法。這意味著我們一般必須在基礎類的接口裡添加某些東西。因此,我們目前面臨的困境是一方面需要向基礎類添加方法,另一方面又不能改動基礎類。怎樣解決這個問題呢?
“訪問器”(Visitor)范式使我們能擴展基本類型的接口,方法是創建類型為Visitor的一個獨立的類結構,對以後需對基本類型采取的操作進行虛擬。基本類型的任務就是簡單地“接收”訪問器,然後調用訪問器的動態綁定方法。看起來就象下面這樣:
現在,假如v是一個指向Aluminum(鋁制品)的Visitable句柄,那麼下述代碼:
PriceVisitor pv = new PriceVisitor();
v.accept(pv);
會造成兩個多形性方法調用:第一個會選擇accept()的Aluminum版本;第二個則在accept()裡——用基礎類Visitor句柄v動態調用visit()的特定版本時。
這種配置意味著可采取Visitor的新子類的形式將新的功能添加到系統裡,沒必要接觸Trash結構。這就是“訪問器”范式最主要的優點:可為一個類結構添加新的多形性功能,同時不必改動結構——只要安裝好了accept()方法。注意這個優點在這兒是有用的,但並不一定是我們在任何情況下的首選方案。所以在最開始的時候,就要判斷這到底是不是自己需要的方案。
現在注意一件沒有做成的事情:訪問器方案防止了從主控Trash序列向單獨類型序列的歸類。所以我們可將所有東西都留在單主控序列中,只需用適當的訪問器通過那個序列傳遞,即可達到希望的目標。盡管這似乎並非訪問器范式的本意,但確實讓我們達到了很希望達到的一個目標(避免使用RTTI)。
訪問器范式中的雙生派遣負責同時判斷Trash以及Visitor的類型。在下面的例子中,大家可看到Visitor的兩種實現方式:PriceVisitor用於判斷總計及價格,而WeightVisitor用於跟蹤重量。
可以看到,所有這些都是用回收程序一個新的、改進過的版本實現的。而且和DoubleDispatch.java一樣,Trash類被保持孤立,並創建一個新接口來添加accept()方法:
//: Visitable.java // An interface to add visitor functionality to // the Trash hierarchy without modifying the // base class. package c16.trashvisitor; import c16.trash.*; interface Visitable { // The new method: void accept(Visitor v); } ///:~
Aluminum,Paper,Glass以及Cardboard的子類型實現了accept()方法:
//: VAluminum.java // Aluminum for the visitor pattern package c16.trashvisitor; import c16.trash.*; public class VAluminum extends Aluminum implements Visitable { public VAluminum(double wt) { super(wt); } public void accept(Visitor v) { v.visit(this); } } ///:~
//: VPaper.java // Paper for the visitor pattern package c16.trashvisitor; import c16.trash.*; public class VPaper extends Paper implements Visitable { public VPaper(double wt) { super(wt); } public void accept(Visitor v) { v.visit(this); } } ///:~
//: VGlass.java // Glass for the visitor pattern package c16.trashvisitor; import c16.trash.*; public class VGlass extends Glass implements Visitable { public VGlass(double wt) { super(wt); } public void accept(Visitor v) { v.visit(this); } } ///:~
//: VCardboard.java // Cardboard for the visitor pattern package c16.trashvisitor; import c16.trash.*; public class VCardboard extends Cardboard implements Visitable { public VCardboard(double wt) { super(wt); } public void accept(Visitor v) { v.visit(this); } } ///:~
由於Visitor基礎類沒有什麼需要實在的東西,可將其創建成一個接口:
//: Visitor.java // The base interface for visitors package c16.trashvisitor; import c16.trash.*; interface Visitor { void visit(VAluminum a); void visit(VPaper p); void visit(VGlass g); void visit(VCardboard c); } ///:~
c16.TrashVisitor.VGlass:54 c16.TrashVisitor.VPaper:22 c16.TrashVisitor.VPaper:11 c16.TrashVisitor.VGlass:17 c16.TrashVisitor.VAluminum:89 c16.TrashVisitor.VPaper:88 c16.TrashVisitor.VAluminum:76 c16.TrashVisitor.VCardboard:96 c16.TrashVisitor.VAluminum:25 c16.TrashVisitor.VAluminum:34 c16.TrashVisitor.VGlass:11 c16.TrashVisitor.VGlass:68 c16.TrashVisitor.VGlass:43 c16.TrashVisitor.VAluminum:27 c16.TrashVisitor.VCardboard:44 c16.TrashVisitor.VAluminum:18 c16.TrashVisitor.VPaper:91 c16.TrashVisitor.VGlass:63 c16.TrashVisitor.VGlass:50 c16.TrashVisitor.VGlass:80 c16.TrashVisitor.VAluminum:81 c16.TrashVisitor.VCardboard:12 c16.TrashVisitor.VGlass:12 c16.TrashVisitor.VGlass:54 c16.TrashVisitor.VAluminum:36 c16.TrashVisitor.VAluminum:93 c16.TrashVisitor.VGlass:93 c16.TrashVisitor.VPaper:80 c16.TrashVisitor.VGlass:36 c16.TrashVisitor.VGlass:12 c16.TrashVisitor.VGlass:60 c16.TrashVisitor.VPaper:66 c16.TrashVisitor.VAluminum:36 c16.TrashVisitor.VCardboard:22
程序剩余的部分將創建特定的Visitor類型,並通過一個Trash對象列表發送它們:
//: TrashVisitor.java // The "visitor" pattern package c16.trashvisitor; import c16.trash.*; import java.util.*; // Specific group of algorithms packaged // in each implementation of Visitor: class PriceVisitor implements Visitor { private double alSum; // Aluminum private double pSum; // Paper private double gSum; // Glass private double cSum; // Cardboard public void visit(VAluminum al) { double v = al.weight() * al.value(); System.out.println( "value of Aluminum= " + v); alSum += v; } public void visit(VPaper p) { double v = p.weight() * p.value(); System.out.println( "value of Paper= " + v); pSum += v; } public void visit(VGlass g) { double v = g.weight() * g.value(); System.out.println( "value of Glass= " + v); gSum += v; } public void visit(VCardboard c) { double v = c.weight() * c.value(); System.out.println( "value of Cardboard = " + v); cSum += v; } void total() { System.out.println( "Total Aluminum: ___FCKpd___7quot; + alSum + "\n" + "Total Paper: ___FCKpd___7quot; + pSum + "\n" + "Total Glass: ___FCKpd___7quot; + gSum + "\n" + "Total Cardboard: ___FCKpd___7quot; + cSum); } } class WeightVisitor implements Visitor { private double alSum; // Aluminum private double pSum; // Paper private double gSum; // Glass private double cSum; // Cardboard public void visit(VAluminum al) { alSum += al.weight(); System.out.println("weight of Aluminum = " + al.weight()); } public void visit(VPaper p) { pSum += p.weight(); System.out.println("weight of Paper = " + p.weight()); } public void visit(VGlass g) { gSum += g.weight(); System.out.println("weight of Glass = " + g.weight()); } public void visit(VCardboard c) { cSum += c.weight(); System.out.println("weight of Cardboard = " + c.weight()); } void total() { System.out.println("Total weight Aluminum:" + alSum); System.out.println("Total weight Paper:" + pSum); System.out.println("Total weight Glass:" + gSum); System.out.println("Total weight Cardboard:" + cSum); } } public class TrashVisitor { public static void main(String[] args) { Vector bin = new Vector(); // ParseTrash still works, without changes: ParseTrash.fillBin("VTrash.dat", bin); // You could even iterate through // a list of visitors! PriceVisitor pv = new PriceVisitor(); WeightVisitor wv = new WeightVisitor(); Enumeration it = bin.elements(); while(it.hasMoreElements()) { Visitable v = (Visitable)it.nextElement(); v.accept(pv); v.accept(wv); } pv.total(); wv.total(); } } ///:~
注意main()的形狀已再次發生了變化。現在只有一個垃圾(Trash)筒。兩個Visitor對象被接收到序列中的每個元素內,它們會完成自己份內的工作。Visitor跟蹤它們自己的內部數據,計算出總重和價格。
最好,將東西從序列中取出的時候,除了不可避免地向Trash造型以外,再沒有運行期的類型驗證。若在Java裡實現了參數化類型,甚至那個造型操作也可以避免。
對比之前介紹過的雙重派遣方案,區分這兩種方案的一個辦法是:在雙重派遣方案中,每個子類創建時只會過載其中的一個過載方法,即add()。而在這裡,每個過載的visit()方法都必須在Visitor的每個子類中進行過載。
1. 更多的結合?
這裡還有其他許多代碼,Trash結構和Visitor結構之間存在著明顯的“結合”(Coupling)關系。然而,在它們所代表的類集內部,也存在著高度的凝聚力:都只做一件事情(Trash描述垃圾或廢品,而Visitor描述對垃圾采取什麼行動)。作為一套優秀的設計方案,這無疑是個良好的開端。當然就目前的情況來說,只有在我們添加新的Visitor類型時才能體會到它的好處。但在添加新類型的Trash時,它卻顯得有些礙手礙腳。
類與類之間低度的結合與類內高度的凝聚無疑是一個重要的設計目標。但只要稍不留神,就可能妨礙我們得到一個本該更出色的設計。從表面看,有些類不可避免地相互間存在著一些“親密”關系。這種關系通常是成對發生的,可以叫作“對聯”(Couplet)——比如集合和繼承器(Enumeration)。前面的Trash-Visitor對似乎也是這樣的一種“對聯”。