访问者模式(学习笔记)
1. 意图
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作
2. 动机
假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。图像中的每个节点既能代表复杂实体(例如一座城市),也能代表更精细的对象(例如工业区和旅游景点等)。如果节点代表的真实对象之间存在公路,那么这些节点就会相互连接。在程序内部,每个节点的类型都由其所属的类来表示,每个特定的节点则是一个对象。
一段时间后,接到了实现将图像导出到XML文件中的任务。这些工作最初看上去非常简单。你计划为每个节点类添加导出函数,然后递归执行图像中每个节点的导出函数。解决方案简单且优雅:使用多态机制可以让导出方法的调用代码不会和具体的节点类相耦合。但s是系统架构师拒绝批准对已有节点类进行修改。他认为这些代码已经是产品了,不想冒险对其进行修改,因为修改可能会引入潜在的缺陷。
此外,他还质疑在节点类中包含导出XML文件的代码是否有意义。这些类的主要工作是处理地理数据,导出XML文件的代码放在这里并不合适。还有另一个原因,那就是在此项任务完成后,营销部门很有可能会要求程序提供导出其他类型文件的功能,或者提出其他奇怪的要求。这样你很可能会被迫再次修改这些重要但脆弱的类。
访问者模式建议将新行为放入一个名为访问者的独立类中,而不是试图将其整合到已有类中。现在,需要执行操作的原始对象将作为参数被传递给访问者中的方法,让方法能访问对象所包含的一切必要数据。
如果现在该操作能在不同类的对象上执行会怎么样呢?比如在我们的示例中,各节点类导出XML文件的实际实现很可能会稍有不同。因此,访问者类可以定义一组(而不是一个)方法,且每个方法可接收不同类型的参数,如下所示:
class ExportVisitor implements Visitor is
method doForCity(City c) { ... }
method doForIndustry(Industry f) { ... }
method doForSightSeeing(SightSeeing ss) { ... }
// ...
问题是,我们该如何调用这些方法呢?可以发现,这些方法的签名各不相同,因此不能使用多态机制。为了可以挑选出能够处理特定对象的访问者方法,我们需要对它的类进行检查,如下面代码所示:
foreach (Node node in graph)
if (node instanceof City)
exportVisitor.doForCity((City) node)
if (node instanceof Industry)
exportVisitor.doForIndustry((Industry) node)
// ...
}
可否使用方法地重载呢?依然不行,因为我们无法提前知晓节点对象所属的类,所以重载机制无法执行正确的方法
访问者模式可以解决这个问题。它使用了一种名为双分派(在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时型别(Run time type),还要根据参数的运行时型别(Run time type)。这里的消息接收者其实就是方法的调用者。具体来讲就是,对于消息表达式a.m(b),双分派能够按照a和b的实际类型为其绑定对应方法体)的技巧,在不使用累赘的条件语句地情况下,也可以执行正确的方法。与其让客户端来选择调用正确版本的方法,不如将对象放在各个节点中,并将访问者对象作为参数传给该对象。由于该对象知晓其自身的类,因此能更自然地在访问者中选出正确的方法。它们会 “接收” 一个访问者并告诉其应执行的访问者方法
// 客户端代码
foreach (Node node in graph)
node.accept(exportVisitor)
// 城市
class City is
method accept(Visitor v) is
v.doForCity(this)
// ...
// 工业区
class Industry is
method accept(Visitor v) is
v.doForIndustry(this)
// ...
我们最终还是修改了节点类,但毕竟改动很小,而且在后续进一步添加行为时无需再次修改代码。现在,如果我们抽取出所有访问者的通用接口,所有已有的节点都能与我们在程序中引入的任何访问者交互。如果需要引入与节点相关的某个行为,你只需要实现一个新的访问者类即可
3. 适用性
- 一个对象结构(如对象树)包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作
- 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作污染这些类对象。Visitor使得你可以将相关操作集中起来定义在一个类中。当对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作
- 当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可使用该模式
4. 结构
5. 效果
1. 访问者模式使得易于增加新的操作(开闭原则)
2. 访问者集中相关的操作而分离无关的操作(单一职责原则)
3. 增加新的ConcreteElement类很困难 Visitor模式使得难以增加新的Element的子类。每添加一个新的ConcreteElement都要在Visitor中添加一个新的抽象操作,并在每一个ConcreteVisitor类中实现相应的操作。所以在使用访问者模式时考虑的关键问题是系统的哪个部分会经常变化,是作用于对象结构上的算法还是构成该结构的给个对象的类。如果总是有新的ConcreteElement类加进来,Visitor类层次将变得难以维护
4. 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限
6. 代码实现
在本例中, 我们希望将一系列几何形状导出为 XML 文件。 重点在于我们不希望直接修改形状代码, 或者至少能确保最小程度的修改。
shapes/Shape.java: 通用形状接口
package visitor.shapes; import visitor.visitor.Visitor; /** * @author GaoMing * @date 2021/7/26 - 17:23 */ public interface Shape { void move(int x, int y); void draw(); String accept(Visitor visitor); }
shapes/Dot.java: 点
package visitor.shapes; import visitor.visitor.Visitor; /** * @author GaoMing * @date 2021/7/26 - 17:24 */ public class Dot implements Shape{ private int id; private int x; private int y; public Dot() { } public Dot(int id, int x, int y) { this.id = id; this.x = x; this.y = y; } @Override public void move(int x, int y) { // move shape } @Override public void draw() { // draw shape } @Override public String accept(Visitor visitor) { return visitor.visitDot(this); } public int getX() { return x; } public int getY() { return y; } public int getId() { return id; } }
shapes/Circle.java: 圆形
package visitor.shapes; import visitor.visitor.Visitor; /** * @author GaoMing * @date 2021/7/26 - 17:25 */ public class Circle extends Dot{ private int radius; public Circle(int id, int x, int y, int radius) { super(id, x, y); this.radius = radius; } @Override public String accept(Visitor visitor) { return visitor.visitCircle(this); } public int getRadius() { return radius; } }
shapes/Rectangle.java: 矩形
package visitor.shapes; import visitor.visitor.Visitor; /** * @author GaoMing * @date 2021/7/26 - 17:26 */ public class Rectangle implements Shape{ private int id; private int x; private int y; private int width; private int height; public Rectangle(int id, int x, int y, int width, int height) { this.id = id; this.x = x; this.y = y; this.width = width; this.height = height; } @Override public String accept(Visitor visitor) { return visitor.visitRectangle(this); } @Override public void move(int x, int y) { // move shape } @Override public void draw() { // draw shape } public int getId() { return id; } public int getX() { return x; } public int getY() { return y; } public int getWidth() { return width; } public int getHeight() { return height; } }
shapes/CompoundShape.java: 组合形状
package visitor.shapes; import visitor.visitor.Visitor; import java.util.ArrayList; import java.util.List; /** * @author GaoMing * @date 2021/7/26 - 17:27 */ public class CompoundShape implements Shape{ public int id; public List<Shape> children = new ArrayList<>(); public CompoundShape(int id) { this.id = id; } @Override public void move(int x, int y) { // move shape } @Override public void draw() { // draw shape } public int getId() { return id; } @Override public String accept(Visitor visitor) { return visitor.visitCompoundGraphic(this); } public void add(Shape shape) { children.add(shape); } }
visitor/Visitor.java: 通用访问者接口
package visitor.visitor; import visitor.shapes.Circle; import visitor.shapes.CompoundShape; import visitor.shapes.Dot; import visitor.shapes.Rectangle; /** * @author GaoMing * @date 2021/7/26 - 17:24 */ public interface Visitor { String visitDot(Dot dot); String visitCircle(Circle circle); String visitRectangle(Rectangle rectangle); String visitCompoundGraphic(CompoundShape cg); }
visitor/XMLExportVisitor.java: 具体访问者,将所有形状导出为 XML 文件
package visitor.visitor; import visitor.shapes.*; /** * @author GaoMing * @date 2021/7/26 - 17:28 */ public class XMLExportVisitor implements Visitor{ public String export(Shape... args) { StringBuilder sb = new StringBuilder(); sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>" + "\n"); for (Shape shape : args) { sb.append(shape.accept(this)).append("\n"); } return sb.toString(); } public String visitDot(Dot d) { return "<dot>" + "\n" + " <id>" + d.getId() + "</id>" + "\n" + " <x>" + d.getX() + "</x>" + "\n" + " <y>" + d.getY() + "</y>" + "\n" + "</dot>"; } public String visitCircle(Circle c) { return "<circle>" + "\n" + " <id>" + c.getId() + "</id>" + "\n" + " <x>" + c.getX() + "</x>" + "\n" + " <y>" + c.getY() + "</y>" + "\n" + " <radius>" + c.getRadius() + "</radius>" + "\n" + "</circle>"; } public String visitRectangle(Rectangle r) { return "<rectangle>" + "\n" + " <id>" + r.getId() + "</id>" + "\n" + " <x>" + r.getX() + "</x>" + "\n" + " <y>" + r.getY() + "</y>" + "\n" + " <width>" + r.getWidth() + "</width>" + "\n" + " <height>" + r.getHeight() + "</height>" + "\n" + "</rectangle>"; } public String visitCompoundGraphic(CompoundShape cg) { return "<compound_graphic>" + "\n" + " <id>" + cg.getId() + "</id>" + "\n" + _visitCompoundGraphic(cg) + "</compound_graphic>"; } private String _visitCompoundGraphic(CompoundShape cg) { StringBuilder sb = new StringBuilder(); for (Shape shape : cg.children) { String obj = shape.accept(this); // Proper indentation for sub-objects. obj = " " + obj.replace("\n", "\n ") + "\n"; sb.append(obj); } return sb.toString(); } }
Demo.java: 客户端代码
package visitor; import visitor.shapes.*; import visitor.visitor.XMLExportVisitor; /** * @author GaoMing * @date 2021/7/26 - 17:23 */ public class Demo { public static void main(String[] args) { Dot dot = new Dot(1, 10, 55); Circle circle = new Circle(2, 23, 15, 10); Rectangle rectangle = new Rectangle(3, 10, 17, 20, 30); CompoundShape compoundShape = new CompoundShape(4); compoundShape.add(dot); compoundShape.add(circle); compoundShape.add(rectangle); CompoundShape c = new CompoundShape(5); c.add(dot); compoundShape.add(c); export(circle, compoundShape); } private static void export(Shape... shapes) { XMLExportVisitor exportVisitor = new XMLExportVisitor(); System.out.println(exportVisitor.export(shapes)); } }
运行结果
<?xml version="1.0" encoding="utf-8"?> <circle> <id>2</id> <x>23</x> <y>15</y> <radius>10</radius> </circle> <?xml version="1.0" encoding="utf-8"?> <compound_graphic> <id>4</id> <dot> <id>1</id> <x>10</x> <y>55</y> </dot> <circle> <id>2</id> <x>23</x> <y>15</y> <radius>10</radius> </circle> <rectangle> <id>3</id> <x>10</x> <y>17</y> <width>20</width> <height>30</height> </rectangle> <compound_graphic> <id>5</id> <dot> <id>1</id> <x>10</x> <y>55</y> </dot> </compound_graphic> </compound_graphic>
7. 与其他模式的关系
- 可以将访问者模式视为命令模式的加强版本,其对象可对不同类的多种对象执行操作
- 可以使用访问者对整个组合模式树执行操作
- 可以同时使用访问者和迭代器模式来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。而迭代器能够访问的所有元素都有一个共同的父类
8. 已知应用
使用示例:访问者不是常用的设计模式,因为它不仅复杂,应用范围也比较狭窄
这里是 Java 程序库代码中该模式的一些示例:
javax.lang.model.element.AnnotationValue 和 AnnotationValueVisitor
javax.lang.model.element.Element 和 ElementVisitor
javax.lang.model.type.TypeMirror 和 TypeVisitor
java.nio.file.FileVisitor 和 SimpleFileVisitor
javax.faces.component.visit.VisitContext 和 VisitCallback