16.7 访问器模式

接下来,让我们思考如何将具有完全不同目标的一个设计模式应用到垃圾归类系统。

对这个模式,我们不再关心在系统中加入新型Trash时的优化。事实上,这个模式使新型Trash的添加显得更加复杂。假定我们有一个基本类结构,它是固定不变的;它或许来自另一个开发者或公司,我们无权对那个结构进行任何修改。然而,我们又希望在那个结构里加入新的多态性方法。这意味着我们一般必须在基类的接口里添加某些东西。因此,我们目前面临的困境是一方面需要向基类添加方法,另一方面又不能改动基类。怎样解决这个问题呢?

“访问器”(Visitor)模式使我们能扩展基本类型的接口,方法是创建类型为Visitor的一个独立的类结构,对以后需对基本类型采取的操作进行虚拟。基本类型的任务就是简单地“接收”访问器,然后调用访问器的动态绑定方法。看起来就象下面这样:

16.7 访问器模式 - 图1

现在,假如v是一个指向Aluminum(铝制品)的Visitable引用,那么下述代码:

  1. PriceVisitor pv = new PriceVisitor();
  2. v.accept(pv);

会造成两个多态性方法调用:第一个会选择accept()Aluminum版本;第二个则在accept()里——用基类Visitor引用v动态调用visit()的特定版本时。

这种配置意味着可采取Visitor的新子类的形式将新的功能添加到系统里,没必要接触Trash结构。这就是“访问器”模式最主要的优点:可为一个类结构添加新的多态性功能,同时不必改动结构——只要安装好了accept()方法。注意这个优点在这儿是有用的,但并不一定是我们在任何情况下的首选方案。所以在最开始的时候,就要判断这到底是不是自己需要的方案。

现在注意一件没有做成的事情:访问器方案防止了从主控Trash序列向单独类型序列的归类。所以我们可将所有东西都留在单主控序列中,只需用适当的访问器通过那个序列传递,即可达到希望的目标。尽管这似乎并非访问器模式的本意,但确实让我们达到了很希望达到的一个目标(避免使用RTTI)。

访问器模式中的双生分发负责同时判断Trash以及Visitor的类型。在下面的例子中,大家可看到Visitor的两种实现方式:PriceVisitor用于判断总计及价格,而WeightVisitor用于跟踪重量。

可以看到,所有这些都是用回收程序一个新的、改进过的版本实现的。而且和DoubleDispatch.java一样,Trash类被保持孤立,并创建一个新接口来添加accept()方法:

  1. //: Visitable.java
  2. // An interface to add visitor functionality to
  3. // the Trash hierarchy without modifying the
  4. // base class.
  5. package c16.trashvisitor;
  6. import c16.trash.*;
  7. interface Visitable {
  8. // The new method:
  9. void accept(Visitor v);
  10. } ///:~

AluminumPaperGlass以及Cardboard的子类型实现了accept()方法:

  1. //: VAluminum.java
  2. // Aluminum for the visitor pattern
  3. package c16.trashvisitor;
  4. import c16.trash.*;
  5. public class VAluminum extends Aluminum
  6. implements Visitable {
  7. public VAluminum(double wt) { super(wt); }
  8. public void accept(Visitor v) {
  9. v.visit(this);
  10. }
  11. } ///:~
  12. //: VPaper.java
  13. // Paper for the visitor pattern
  14. package c16.trashvisitor;
  15. import c16.trash.*;
  16. public class VPaper extends Paper
  17. implements Visitable {
  18. public VPaper(double wt) { super(wt); }
  19. public void accept(Visitor v) {
  20. v.visit(this);
  21. }
  22. } ///:~
  23. //: VGlass.java
  24. // Glass for the visitor pattern
  25. package c16.trashvisitor;
  26. import c16.trash.*;
  27. public class VGlass extends Glass
  28. implements Visitable {
  29. public VGlass(double wt) { super(wt); }
  30. public void accept(Visitor v) {
  31. v.visit(this);
  32. }
  33. } ///:~
  34. //: VCardboard.java
  35. // Cardboard for the visitor pattern
  36. package c16.trashvisitor;
  37. import c16.trash.*;
  38. public class VCardboard extends Cardboard
  39. implements Visitable {
  40. public VCardboard(double wt) { super(wt); }
  41. public void accept(Visitor v) {
  42. v.visit(this);
  43. }
  44. } ///:~

由于Visitor基类没有什么需要实在的东西,可将其创建成一个接口:

  1. //: Visitor.java
  2. // The base interface for visitors
  3. package c16.trashvisitor;
  4. import c16.trash.*;
  5. interface Visitor {
  6. void visit(VAluminum a);
  7. void visit(VPaper p);
  8. void visit(VGlass g);
  9. void visit(VCardboard c);
  10. } ///:~
  11. c16.TrashVisitor.VGlass:54
  12. c16.TrashVisitor.VPaper:22
  13. c16.TrashVisitor.VPaper:11
  14. c16.TrashVisitor.VGlass:17
  15. c16.TrashVisitor.VAluminum:89
  16. c16.TrashVisitor.VPaper:88
  17. c16.TrashVisitor.VAluminum:76
  18. c16.TrashVisitor.VCardboard:96
  19. c16.TrashVisitor.VAluminum:25
  20. c16.TrashVisitor.VAluminum:34
  21. c16.TrashVisitor.VGlass:11
  22. c16.TrashVisitor.VGlass:68
  23. c16.TrashVisitor.VGlass:43
  24. c16.TrashVisitor.VAluminum:27
  25. c16.TrashVisitor.VCardboard:44
  26. c16.TrashVisitor.VAluminum:18
  27. c16.TrashVisitor.VPaper:91
  28. c16.TrashVisitor.VGlass:63
  29. c16.TrashVisitor.VGlass:50
  30. c16.TrashVisitor.VGlass:80
  31. c16.TrashVisitor.VAluminum:81
  32. c16.TrashVisitor.VCardboard:12
  33. c16.TrashVisitor.VGlass:12
  34. c16.TrashVisitor.VGlass:54
  35. c16.TrashVisitor.VAluminum:36
  36. c16.TrashVisitor.VAluminum:93
  37. c16.TrashVisitor.VGlass:93
  38. c16.TrashVisitor.VPaper:80
  39. c16.TrashVisitor.VGlass:36
  40. c16.TrashVisitor.VGlass:12
  41. c16.TrashVisitor.VGlass:60
  42. c16.TrashVisitor.VPaper:66
  43. c16.TrashVisitor.VAluminum:36
  44. c16.TrashVisitor.VCardboard:22

程序剩余的部分将创建特定的Visitor类型,并通过一个Trash对象列表发送它们:

  1. //: TrashVisitor.java
  2. // The "visitor" pattern
  3. package c16.trashvisitor;
  4. import c16.trash.*;
  5. import java.util.*;
  6. // Specific group of algorithms packaged
  7. // in each implementation of Visitor:
  8. class PriceVisitor implements Visitor {
  9. private double alSum; // Aluminum
  10. private double pSum; // Paper
  11. private double gSum; // Glass
  12. private double cSum; // Cardboard
  13. public void visit(VAluminum al) {
  14. double v = al.weight() * al.value();
  15. System.out.println(
  16. "value of Aluminum= " + v);
  17. alSum += v;
  18. }
  19. public void visit(VPaper p) {
  20. double v = p.weight() * p.value();
  21. System.out.println(
  22. "value of Paper= " + v);
  23. pSum += v;
  24. }
  25. public void visit(VGlass g) {
  26. double v = g.weight() * g.value();
  27. System.out.println(
  28. "value of Glass= " + v);
  29. gSum += v;
  30. }
  31. public void visit(VCardboard c) {
  32. double v = c.weight() * c.value();
  33. System.out.println(
  34. "value of Cardboard = " + v);
  35. cSum += v;
  36. }
  37. void total() {
  38. System.out.println(
  39. "Total Aluminum: $" + alSum + "\n" +
  40. "Total Paper: $" + pSum + "\n" +
  41. "Total Glass: $" + gSum + "\n" +
  42. "Total Cardboard: $" + cSum);
  43. }
  44. }
  45. class WeightVisitor implements Visitor {
  46. private double alSum; // Aluminum
  47. private double pSum; // Paper
  48. private double gSum; // Glass
  49. private double cSum; // Cardboard
  50. public void visit(VAluminum al) {
  51. alSum += al.weight();
  52. System.out.println("weight of Aluminum = "
  53. + al.weight());
  54. }
  55. public void visit(VPaper p) {
  56. pSum += p.weight();
  57. System.out.println("weight of Paper = "
  58. + p.weight());
  59. }
  60. public void visit(VGlass g) {
  61. gSum += g.weight();
  62. System.out.println("weight of Glass = "
  63. + g.weight());
  64. }
  65. public void visit(VCardboard c) {
  66. cSum += c.weight();
  67. System.out.println("weight of Cardboard = "
  68. + c.weight());
  69. }
  70. void total() {
  71. System.out.println("Total weight Aluminum:"
  72. + alSum);
  73. System.out.println("Total weight Paper:"
  74. + pSum);
  75. System.out.println("Total weight Glass:"
  76. + gSum);
  77. System.out.println("Total weight Cardboard:"
  78. + cSum);
  79. }
  80. }
  81. public class TrashVisitor {
  82. public static void main(String[] args) {
  83. Vector bin = new Vector();
  84. // ParseTrash still works, without changes:
  85. ParseTrash.fillBin("VTrash.dat", bin);
  86. // You could even iterate through
  87. // a list of visitors!
  88. PriceVisitor pv = new PriceVisitor();
  89. WeightVisitor wv = new WeightVisitor();
  90. Enumeration it = bin.elements();
  91. while(it.hasMoreElements()) {
  92. Visitable v = (Visitable)it.nextElement();
  93. v.accept(pv);
  94. v.accept(wv);
  95. }
  96. pv.total();
  97. wv.total();
  98. }
  99. } ///:~

注意main()的形状已再次发生了变化。现在只有一个垃圾(Trash)筒。两个Visitor对象被接收到序列中的每个元素内,它们会完成自己份内的工作。Visitor跟踪它们自己的内部数据,计算出总重和价格。

最好,将东西从序列中取出的时候,除了不可避免地向Trash转换以外,再没有运行期的类型验证。若在Java里实现了参数化类型,甚至那个转换操作也可以避免。

对比之前介绍过的双重分发方案,区分这两种方案的一个办法是:在双重分发方案中,每个子类创建时只会重载其中的一个重载方法,即add()。而在这里,每个重载的visit()方法都必须在Visitor的每个子类中进行重载。

(1) 更多的结合?

这里还有其他许多代码,Trash结构和Visitor结构之间存在着明显的“结合”(Coupling)关系。然而,在它们所代表的类集内部,也存在着高度的凝聚力:都只做一件事情(Trash描述垃圾或废品,而Visitor描述对垃圾采取什么行动)。作为一套优秀的设计模式,这无疑是个良好的开端。当然就目前的情况来说,只有在我们添加新的Visitor类型时才能体会到它的好处。但在添加新类型的Trash时,它却显得有些碍手碍脚。

类与类之间低度的结合与类内高度的凝聚无疑是一个重要的设计目标。但只要稍不留神,就可能妨碍我们得到一个本该更出色的设计。从表面看,有些类不可避免地相互间存在着一些“亲密”关系。这种关系通常是成对发生的,可以叫作“对联”(Couplet)——比如集合和迭代器(Enumeration)。前面的Trash-Visitor对似乎也是这样的一种“对联”。