访问者模式:在不修改类的情况下扩展功能
-
-
- 问题场景:图形处理系统的挑战
-
- 1.1 传统做法的问题
-
- 访问者模式:优雅的解决方案
-
- 2.1 访问者模式的核心思想
-
- 访问者模式的完整实现
-
- 3.1 第一步:定义可访问的元素接口
- 3.2 第二步:重构具体元素类
- 3.3 第三步:定义访问者接口
- 3.4 第四步:实现具体访问者
- 3.5 第五步:图形集合类
- 3.6 第六步:客户端使用示例
-
- 访问者模式的核心组件
-
- 4.1 双重分派(Double Dispatch)
- 4.2 访问者模式类图
-
- 访问者模式的变体
-
- 5.1 带返回值的访问者
- 5.2 带参数的访问者
- 5.3 带上下文的访问者
-
- 访问者模式的优缺点
-
- 6.1 优点
- 6.2 缺点
-
- 访问者模式的实际应用
-
- 7.1 Java编译器中的AST访问
- 7.2 文件系统访问
- 7.3 数据库操作
-
- 访问者模式与其他模式的关系
-
- 8.1 与组合模式的结合
- 8.2 与迭代器模式的比较
-
- 何时使用访问者模式
-
- 9.1 适用场景
- 9.2 不适用场景
-
- 访问者模式的最佳实践
-
- 10.1 设计建议
- 10.2 性能考虑
- 10.3 测试访问者
-
- 总结
-
- 11.1 核心价值
- 11.2 关键要点
- 11.3 使用建议
-
1. 问题场景:图形处理系统的挑战
假设我们正在开发一个图形编辑系统,其中包含多种图形元素:
java
// 图形基类
public abstract class Shape {
public abstract void draw();
}
// 圆形
public class Circle extends Shape {
private double radius;
private Point center;
public Circle(double radius, Point center) {
this.radius = radius;
this.center = center;
}
public double getRadius() { return radius; }
public Point getCenter() { return center; }
@Override
public void draw() {
System.out.println("Drawing a circle with radius " + radius);
}
}
// 矩形
public class Rectangle extends Shape {
private double width;
private double height;
private Point topLeft;
public Rectangle(double width, double height, Point topLeft) {
this.width = width;
this.height = height;
this.topLeft = topLeft;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
public Point getTopLeft() { return topLeft; }
@Override
public void draw() {
System.out.println("Drawing a rectangle " + width + "x" + height);
}
}
// 三角形
public class Triangle extends Shape {
private Point point1;
private Point point2;
private Point point3;
public Triangle(Point p1, Point p2, Point p3) {
this.point1 = p1;
this.point2 = p2;
this.point3 = p3;
}
public Point getPoint1() { return point1; }
public Point getPoint2() { return point2; }
public Point getPoint3() { return point3; }
@Override
public void draw() {
System.out.println("Drawing a triangle");
}
}
现在,我们需要为这些图形添加新功能:
- 计算面积
- 导出为XML
- 碰撞检测
- 序列化为JSON
1.1 传统做法的问题
方案一:在Shape基类中添加方法
java
// 不推荐的写法:违反开闭原则
public abstract class Shape {
public abstract void draw();
// 添加新方法
public abstract double calculateArea();
public abstract String exportToXML();
public abstract boolean checkCollision(Shape other);
public abstract String toJSON();
}
问题:
- 每次添加新功能,都需要修改所有Shape子类
- 违反了开闭原则(对扩展开放,对修改关闭)
- 如果Shape有几十个子类,工作量巨大
方案二:使用instanceof判断
java
// 不推荐的写法:类型检查,难以维护
public class AreaCalculator {
public double calculateArea(Shape shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.getRadius() * circle.getRadius();
} else if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
return rect.getWidth() * rect.getHeight();
} else if (shape instanceof Triangle) {
Triangle triangle = (Triangle) shape;
// 使用海伦公式计算三角形面积
double a = distance(triangle.getPoint1(), triangle.getPoint2());
double b = distance(triangle.getPoint2(), triangle.getPoint3());
double c = distance(triangle.getPoint3(), triangle.getPoint1());
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
} else {
throw new IllegalArgumentException("Unknown shape type");
}
}
private double distance(Point p1, Point p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
}
问题:
- 类型检查和强制转换
- 违反开闭原则,新增图形类型需要修改所有方法
- 逻辑分散,难以维护
2. 访问者模式:优雅的解决方案
访问者模式允许你在不修改现有类的情况下,为它们定义新的操作。它通过将操作"访问"逻辑分离到独立的访问者类中来实现。
2.1 访问者模式的核心思想
accept()
visit()
<<interface>>
Visitor
+visitCircle(Circle) : void
+visitRectangle(Rectangle) : void
+visitTriangle(Triangle) : void
<<interface>>
Element
+accept(Visitor) : void
ConcreteVisitor
+visitCircle(Circle) : void
+visitRectangle(Rectangle) : void
+visitTriangle(Triangle) : void
ConcreteElement
+accept(Visitor) : void
访问者模式包含两个核心层次结构:
- 元素层次结构:包含需要被访问的类
- 访问者层次结构:包含对元素进行操作的类
3. 访问者模式的完整实现
3.1 第一步:定义可访问的元素接口
java
// 图形元素接口
public interface Shape {
/**
* 接受访问者的访问
* @param visitor 访问者
*/
void accept(ShapeVisitor visitor);
/**
* 绘制方法(原有的方法保持不变)
*/
void draw();
}
3.2 第二步:重构具体元素类
java
// 点类
public class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public double distance(Point other) {
return Math.sqrt(Math.pow(other.x - this.x, 2) + Math.pow(other.y - this.y, 2));
}
@Override
public String toString() {
return String.format("(%.2f, %.2f)", x, y);
}
}
// 圆形
public class Circle implements Shape {
private double radius;
private Point center;
public Circle(double radius, Point center) {
this.radius = radius;
this.center = center;
}
public double getRadius() { return radius; }
public Point getCenter() { return center; }
@Override
public void draw() {
System.out.println("Drawing a circle with radius " + radius + " at " + center);
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitCircle(this);
}
}
// 矩形
public class Rectangle implements Shape {
private double width;
private double height;
private Point topLeft;
public Rectangle(double width, double height, Point topLeft) {
this.width = width;
this.height = height;
this.topLeft = topLeft;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
public Point getTopLeft() { return topLeft; }
public Point getBottomRight() {
return new Point(topLeft.getX() + width, topLeft.getY() + height);
}
@Override
public void draw() {
System.out.println("Drawing a rectangle " + width + "x" + height + " at " + topLeft);
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitRectangle(this);
}
}
// 三角形
public class Triangle implements Shape {
private Point point1;
private Point point2;
private Point point3;
public Triangle(Point p1, Point p2, Point p3) {
this.point1 = p1;
this.point2 = p2;
this.point3 = p3;
}
public Point getPoint1() { return point1; }
public Point getPoint2() { return point2; }
public Point getPoint3() { return point3; }
@Override
public void draw() {
System.out.println("Drawing a triangle with points: " +
point1 + ", " + point2 + ", " + point3);
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitTriangle(this);
}
}
3.3 第三步:定义访问者接口
java
/**
* 图形访问者接口
* 为每种图形元素定义访问方法
*/
public interface ShapeVisitor {
/**
* 访问圆形
*/
void visitCircle(Circle circle);
/**
* 访问矩形
*/
void visitRectangle(Rectangle rectangle);
/**
* 访问三角形
*/
void visitTriangle(Triangle triangle);
}
3.4 第四步:实现具体访问者
访问者1:面积计算器
java
/**
* 面积计算访问者
*/
public class AreaCalculator implements ShapeVisitor {
private double totalArea = 0;
@Override
public void visitCircle(Circle circle) {
double area = Math.PI * circle.getRadius() * circle.getRadius();
System.out.println("Circle area: " + String.format("%.2f", area));
totalArea += area;
}
@Override
public void visitRectangle(Rectangle rectangle) {
double area = rectangle.getWidth() * rectangle.getHeight();
System.out.println("Rectangle area: " + String.format("%.2f", area));
totalArea += area;
}
@Override
public void visitTriangle(Triangle triangle) {
// 使用海伦公式计算三角形面积
double a = triangle.getPoint1().distance(triangle.getPoint2());
double b = triangle.getPoint2().distance(triangle.getPoint3());
double c = triangle.getPoint3().distance(triangle.getPoint1());
double s = (a + b + c) / 2;
double area = Math.sqrt(s * (s - a) * (s - b) * (s - c));
System.out.println("Triangle area: " + String.format("%.2f", area));
totalArea += area;
}
public double getTotalArea() {
return totalArea;
}
public void reset() {
totalArea = 0;
}
}
访问者2:XML导出器
java
/**
* XML导出访问者
*/
public class XMLExporter implements ShapeVisitor {
private StringBuilder xmlBuilder = new StringBuilder();
public XMLExporter() {
xmlBuilder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xmlBuilder.append("<shapes>\n");
}
@Override
public void visitCircle(Circle circle) {
xmlBuilder.append(" <circle>\n");
xmlBuilder.append(" <radius>").append(circle.getRadius()).append("</radius>\n");
xmlBuilder.append(" <center>\n");
xmlBuilder.append(" <x>").append(circle.getCenter().getX()).append("</x>\n");
xmlBuilder.append(" <y>").append(circle.getCenter().getY()).append("</y>\n");
xmlBuilder.append(" </center>\n");
xmlBuilder.append(" </circle>\n");
}
@Override
public void visitRectangle(Rectangle rectangle) {
xmlBuilder.append(" <rectangle>\n");
xmlBuilder.append(" <width>").append(rectangle.getWidth()).append("</width>\n");
xmlBuilder.append(" <height>").append(rectangle.getHeight()).append("</height>\n");
xmlBuilder.append(" <topLeft>\n");
xmlBuilder.append(" <x>").append(rectangle.getTopLeft().getX()).append("</x>\n");
xmlBuilder.append(" <y>").append(rectangle.getTopLeft().getY()).append("</y>\n");
xmlBuilder.append(" </topLeft>\n");
xmlBuilder.append(" </rectangle>\n");
}
@Override
public void visitTriangle(Triangle triangle) {
xmlBuilder.append(" <triangle>\n");
xmlBuilder.append(" <point1>\n");
xmlBuilder.append(" <x>").append(triangle.getPoint1().getX()).append("</x>\n");
xmlBuilder.append(" <y>").append(triangle.getPoint1().getY()).append("</y>\n");
xmlBuilder.append(" </point1>\n");
xmlBuilder.append(" <point2>\n");
xmlBuilder.append(" <x>").append(triangle.getPoint2().getX()).append("</x>\n");
xmlBuilder.append(" <y>").append(triangle.getPoint2().getY()).append("</y>\n");
xmlBuilder.append(" </point2>\n");
xmlBuilder.append(" <point3>\n");
xmlBuilder.append(" <x>").append(triangle.getPoint3().getX()).append("</x>\n");
xmlBuilder.append(" <y>").append(triangle.getPoint3().getY()).append("</y>\n");
xmlBuilder.append(" </point3>\n");
xmlBuilder.append(" </triangle>\n");
}
public String getXML() {
String xml = xmlBuilder.toString();
return xml + "</shapes>";
}
public void saveToFile(String filename) {
try {
Files.write(Paths.get(filename), getXML().getBytes());
System.out.println("XML saved to: " + filename);
} catch (IOException e) {
System.err.println("Error saving XML: " + e.getMessage());
}
}
}
访问者3:JSON序列化器
java
/**
* JSON序列化访问者
*/
public class JsonExporter implements ShapeVisitor {
private StringBuilder jsonBuilder = new StringBuilder();
private boolean firstElement = true;
public JsonExporter() {
jsonBuilder.append("{\n");
jsonBuilder.append(" \"shapes\": [\n");
}
@Override
public void visitCircle(Circle circle) {
if (!firstElement) {
jsonBuilder.append(",\n");
}
jsonBuilder.append(" {\n");
jsonBuilder.append(" \"type\": \"circle\",\n");
jsonBuilder.append(" \"radius\": ").append(circle.getRadius()).append(",\n");
jsonBuilder.append(" \"center\": {\n");
jsonBuilder.append(" \"x\": ").append(circle.getCenter().getX()).append(",\n");
jsonBuilder.append(" \"y\": ").append(circle.getCenter().getY()).append("\n");
jsonBuilder.append(" }\n");
jsonBuilder.append(" }");
firstElement = false;
}
@Override
public void visitRectangle(Rectangle rectangle) {
if (!firstElement) {
jsonBuilder.append(",\n");
}
jsonBuilder.append(" {\n");
jsonBuilder.append(" \"type\": \"rectangle\",\n");
jsonBuilder.append(" \"width\": ").append(rectangle.getWidth()).append(",\n");
jsonBuilder.append(" \"height\": ").append(rectangle.getHeight()).append(",\n");
jsonBuilder.append(" \"topLeft\": {\n");
jsonBuilder.append(" \"x\": ").append(rectangle.getTopLeft().getX()).append(",\n");
jsonBuilder.append(" \"y\": ").append(rectangle.getTopLeft().getY()).append("\n");
jsonBuilder.append(" }\n");
jsonBuilder.append(" }");
firstElement = false;
}
@Override
public void visitTriangle(Triangle triangle) {
if (!firstElement) {
jsonBuilder.append(",\n");
}
jsonBuilder.append(" {\n");
jsonBuilder.append(" \"type\": \"triangle\",\n");
jsonBuilder.append(" \"points\": [\n");
jsonBuilder.append(" { \"x\": ").append(triangle.getPoint1().getX()).append(", \"y\": ").append(triangle.getPoint1().getY()).append(" },\n");
jsonBuilder.append(" { \"x\": ").append(triangle.getPoint2().getX()).append(", \"y\": ").append(triangle.getPoint2().getY()).append(" },\n");
jsonBuilder.append(" { \"x\": ").append(triangle.getPoint3().getX()).append(", \"y\": ").append(triangle.getPoint3().getY()).append(" }\n");
jsonBuilder.append(" ]\n");
jsonBuilder.append(" }");
firstElement = false;
}
public String getJSON() {
String json = jsonBuilder.toString();
return json + "\n ]\n}";
}
public void printJSON() {
System.out.println(getJSON());
}
}
访问者4:碰撞检测器
java
/**
* 碰撞检测访问者
* 检测与指定图形的碰撞
*/
public class CollisionDetector implements ShapeVisitor {
private Shape targetShape;
private boolean collisionDetected = false;
private Point targetPoint; // 用于点碰撞检测
// 构造器1:与其他图形检测碰撞
public CollisionDetector(Shape targetShape) {
this.targetShape = targetShape;
}
// 构造器2:与点检测碰撞
public CollisionDetector(Point point) {
this.targetPoint = point;
}
@Override
public void visitCircle(Circle circle) {
if (targetShape != null) {
// 与另一个图形检测碰撞
collisionDetected = checkCollisionWithShape(circle, targetShape);
} else if (targetPoint != null) {
// 与点检测碰撞
double distance = circle.getCenter().distance(targetPoint);
collisionDetected = distance <= circle.getRadius();
}
}
@Override
public void visitRectangle(Rectangle rectangle) {
if (targetShape != null) {
// 与另一个图形检测碰撞
collisionDetected = checkCollisionWithShape(rectangle, targetShape);
} else if (targetPoint != null) {
// 与点检测碰撞
double x = targetPoint.getX();
double y = targetPoint.getY();
double rectX = rectangle.getTopLeft().getX();
double rectY = rectangle.getTopLeft().getY();
double width = rectangle.getWidth();
double height = rectangle.getHeight();
collisionDetected = (x >= rectX && x <= rectX + width &&
y >= rectY && y <= rectY + height);
}
}
@Override
public void visitTriangle(Triangle triangle) {
if (targetPoint != null) {
// 与点检测碰撞(使用重心坐标法)
collisionDetected = isPointInTriangle(triangle, targetPoint);
} else if (targetShape != null) {
collisionDetected = checkCollisionWithShape(triangle, targetShape);
}
}
private boolean checkCollisionWithShape(Shape shape1, Shape shape2) {
// 简化的碰撞检测,实际实现会更复杂
System.out.println("Checking collision between " + shape1.getClass().getSimpleName() +
" and " + shape2.getClass().getSimpleName());
// 这里只是示例,实际需要实现具体的碰撞检测算法
return false;
}
private boolean isPointInTriangle(Triangle triangle, Point point) {
// 使用重心坐标法判断点是否在三角形内
Point p1 = triangle.getPoint1();
Point p2 = triangle.getPoint2();
Point p3 = triangle.getPoint3();
double area = 0.5 * (-p2.getY() * p3.getX() + p1.getY() * (-p2.getX() + p3.getX()) +
p1.getX() * (p2.getY() - p3.getY()) + p2.getX() * p3.getY());
double s = 1 / (2 * area) * (p1.getY() * p3.getX() - p1.getX() * p3.getY() +
(p3.getY() - p1.getY()) * point.getX() +
(p1.getX() - p3.getX()) * point.getY());
double t = 1 / (2 * area) * (p1.getX() * p2.getY() - p1.getY() * p2.getX() +
(p1.getY() - p2.getY()) * point.getX() +
(p2.getX() - p1.getX()) * point.getY());
return s > 0 && t > 0 && (1 - s - t) > 0;
}
public boolean isCollisionDetected() {
return collisionDetected;
}
public void reset() {
collisionDetected = false;
}
}
3.5 第五步:图形集合类
java
/**
* 图形集合,可以包含多个图形
*/
public class ShapeCollection {
private List<Shape> shapes = new ArrayList<>();
/**
* 添加图形
*/
public void addShape(Shape shape) {
shapes.add(shape);
}
/**
* 移除图形
*/
public void removeShape(Shape shape) {
shapes.remove(shape);
}
/**
* 获取所有图形
*/
public List<Shape> getShapes() {
return new ArrayList<>(shapes);
}
/**
* 绘制所有图形
*/
public void drawAll() {
System.out.println("Drawing all shapes:");
for (Shape shape : shapes) {
shape.draw();
}
}
/**
* 接受访问者访问所有图形
* 这是访问者模式的关键:遍历所有元素,让它们接受访问
*/
public void accept(ShapeVisitor visitor) {
for (Shape shape : shapes) {
shape.accept(visitor);
}
}
/**
* 批量添加图形
*/
public void addShapes(Shape... shapesToAdd) {
Collections.addAll(shapes, shapesToAdd);
}
}
3.6 第六步:客户端使用示例
java
/**
* 客户端演示
*/
public class VisitorPatternDemo {
public static void main(String[] args) {
System.out.println("===== 访问者模式演示 =====");
// 创建一些图形
Circle circle = new Circle(5.0, new Point(10, 10));
Rectangle rectangle = new Rectangle(8.0, 6.0, new Point(20, 20));
Triangle triangle = new Triangle(
new Point(0, 0),
new Point(10, 0),
new Point(5, 8.66)
);
// 创建图形集合
ShapeCollection shapes = new ShapeCollection();
shapes.addShapes(circle, rectangle, triangle);
// 演示1:计算所有图形的面积
System.out.println("\n1. 计算面积:");
AreaCalculator areaCalculator = new AreaCalculator();
shapes.accept(areaCalculator);
System.out.println("总面积: " + String.format("%.2f", areaCalculator.getTotalArea()));
// 演示2:导出为XML
System.out.println("\n2. 导出为XML:");
XMLExporter xmlExporter = new XMLExporter();
shapes.accept(xmlExporter);
System.out.println(xmlExporter.getXML());
// 演示3:导出为JSON
System.out.println("\n3. 导出为JSON:");
JsonExporter jsonExporter = new JsonExporter();
shapes.accept(jsonExporter);
jsonExporter.printJSON();
// 演示4:碰撞检测
System.out.println("\n4. 碰撞检测:");
// 检测点(12, 12)是否在圆内
CollisionDetector pointCollisionDetector = new CollisionDetector(new Point(12, 12));
circle.accept(pointCollisionDetector);
System.out.println("点(12, 12)是否在圆内: " + pointCollisionDetector.isCollisionDetected());
// 检测点(25, 25)是否在矩形内
pointCollisionDetector.reset();
rectangle.accept(pointCollisionDetector);
System.out.println("点(25, 25)是否在矩形内: " + pointCollisionDetector.isCollisionDetected());
// 检测点(5, 5)是否在三角形内
pointCollisionDetector.reset();
triangle.accept(pointCollisionDetector);
System.out.println("点(5, 5)是否在三角形内: " + pointCollisionDetector.isCollisionDetected());
// 演示5:新增功能而不修改图形类
System.out.println("\n5. 新增功能 - 计算总周长:");
PerimeterCalculator perimeterCalculator = new PerimeterCalculator();
shapes.accept(perimeterCalculator);
System.out.println("总周长: " + String.format("%.2f", perimeterCalculator.getTotalPerimeter()));
// 演示6:统计图形信息
System.out.println("\n6. 统计图形信息:");
ShapeStatistics statistics = new ShapeStatistics();
shapes.accept(statistics);
statistics.printStatistics();
// 演示7:选择性访问
System.out.println("\n7. 只访问圆形:");
CircleOnlyVisitor circleOnlyVisitor = new CircleOnlyVisitor();
shapes.accept(circleOnlyVisitor);
}
}
/**
* 新增的访问者:周长计算器
* 这展示了如何轻松扩展功能
*/
class PerimeterCalculator implements ShapeVisitor {
private double totalPerimeter = 0;
@Override
public void visitCircle(Circle circle) {
double perimeter = 2 * Math.PI * circle.getRadius();
System.out.println("Circle perimeter: " + String.format("%.2f", perimeter));
totalPerimeter += perimeter;
}
@Override
public void visitRectangle(Rectangle rectangle) {
double perimeter = 2 * (rectangle.getWidth() + rectangle.getHeight());
System.out.println("Rectangle perimeter: " + String.format("%.2f", perimeter));
totalPerimeter += perimeter;
}
@Override
public void visitTriangle(Triangle triangle) {
double a = triangle.getPoint1().distance(triangle.getPoint2());
double b = triangle.getPoint2().distance(triangle.getPoint3());
double c = triangle.getPoint3().distance(triangle.getPoint1());
double perimeter = a + b + c;
System.out.println("Triangle perimeter: " + String.format("%.2f", perimeter));
totalPerimeter += perimeter;
}
public double getTotalPerimeter() {
return totalPerimeter;
}
}
/**
* 新增的访问者:图形统计
*/
class ShapeStatistics implements ShapeVisitor {
private int circleCount = 0;
private int rectangleCount = 0;
private int triangleCount = 0;
@Override
public void visitCircle(Circle circle) {
circleCount++;
}
@Override
public void visitRectangle(Rectangle rectangle) {
rectangleCount++;
}
@Override
public void visitTriangle(Triangle triangle) {
triangleCount++;
}
public void printStatistics() {
int total = circleCount + rectangleCount + triangleCount;
System.out.println("图形统计:");
System.out.println(" 圆形数量: " + circleCount);
System.out.println(" 矩形数量: " + rectangleCount);
System.out.println(" 三角形数量: " + triangleCount);
System.out.println(" 总计: " + total);
}
}
/**
* 新增的访问者:只访问圆形
* 这展示了访问者可以有选择地处理某些类型
*/
class CircleOnlyVisitor implements ShapeVisitor {
@Override
public void visitCircle(Circle circle) {
System.out.println("Found a circle with radius: " + circle.getRadius());
}
@Override
public void visitRectangle(Rectangle rectangle) {
// 忽略矩形
}
@Override
public void visitTriangle(Triangle triangle) {
// 忽略三角形
}
}
输出结果:
===== 访问者模式演示 =====
1. 计算面积:
Circle area: 78.54
Rectangle area: 48.00
Triangle area: 43.30
总面积: 169.84
2. 导出为XML:
<?xml version="1.0" encoding="UTF-8"?>
<shapes>
<circle>
<radius>5.0</radius>
<center>
<x>10.0</x>
<y>10.0</y>
</center>
</circle>
<rectangle>
<width>8.0</width>
<height>6.0</height>
<topLeft>
<x>20.0</x>
<y>20.0</y>
</topLeft>
</rectangle>
<triangle>
<point1>
<x>0.0</x>
<y>0.0</y>
</point1>
<point2>
<x>10.0</x>
<y>0.0</y>
</point2>
<point3>
<x>5.0</x>
<y>8.66</y>
</point3>
</triangle>
</shapes>
3. 导出为JSON:
{
"shapes": [
{
"type": "circle",
"radius": 5.0,
"center": {
"x": 10.0,
"y": 10.0
}
},
{
"type": "rectangle",
"width": 8.0,
"height": 6.0,
"topLeft": {
"x": 20.0,
"y": 20.0
}
},
{
"type": "triangle",
"points": [
{ "x": 0.0, "y": 0.0 },
{ "x": 10.0, "y": 0.0 },
{ "x": 5.0, "y": 8.66 }
]
}
]
}
4. 碰撞检测:
点(12, 12)是否在圆内: true
点(25, 25)是否在矩形内: true
点(5, 5)是否在三角形内: false
5. 新增功能 - 计算总周长:
Circle perimeter: 31.42
Rectangle perimeter: 28.00
Triangle perimeter: 30.00
总周长: 89.42
6. 统计图形信息:
图形统计:
圆形数量: 1
矩形数量: 1
三角形数量: 1
总计: 3
7. 只访问圆形:
Found a circle with radius: 5.0
4. 访问者模式的核心组件
4.1 双重分派(Double Dispatch)
访问者模式的关键是双重分派机制:
- 第一次分派 :客户端调用元素的
accept(visitor)方法 - 第二次分派 :元素调用访问者的
visit(this)方法,并将自身(具体类型)作为参数传递
java
// 客户端调用
shape.accept(visitor);
// 在Circle类中
public void accept(ShapeVisitor visitor) {
visitor.visitCircle(this); // 将具体的Circle类型传递给访问者
}
// 在AreaCalculator中
public void visitCircle(Circle circle) {
// 这里知道参数是Circle类型,不需要类型检查
double area = Math.PI * circle.getRadius() * circle.getRadius();
// ...
}
4.2 访问者模式类图
Client
ObjectStructure
<<interface>>
Element
+accept(Visitor) : void
ConcreteElementA
+accept(Visitor) : void
+operationA() : String
ConcreteElementB
+accept(Visitor) : void
+operationB() : String
<<interface>>
Visitor
+visitConcreteElementA(ConcreteElementA) : void
+visitConcreteElementB(ConcreteElementB) : void
ConcreteVisitor1
+visitConcreteElementA(ConcreteElementA) : void
+visitConcreteElementB(ConcreteElementB) : void
ConcreteVisitor2
+visitConcreteElementA(ConcreteElementA) : void
+visitConcreteElementB(ConcreteElementB) : void
5. 访问者模式的变体
5.1 带返回值的访问者
java
/**
* 带返回值的访问者接口
*/
public interface ShapeVisitorWithReturn<T> {
T visitCircle(Circle circle);
T visitRectangle(Rectangle rectangle);
T visitTriangle(Triangle triangle);
}
/**
* 面积计算器(带返回值)
*/
class AreaCalculatorWithReturn implements ShapeVisitorWithReturn<Double> {
@Override
public Double visitCircle(Circle circle) {
return Math.PI * circle.getRadius() * circle.getRadius();
}
@Override
public Double visitRectangle(Rectangle rectangle) {
return rectangle.getWidth() * rectangle.getHeight();
}
@Override
public Double visitTriangle(Triangle triangle) {
double a = triangle.getPoint1().distance(triangle.getPoint2());
double b = triangle.getPoint2().distance(triangle.getPoint3());
double c = triangle.getPoint3().distance(triangle.getPoint1());
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
}
5.2 带参数的访问者
java
/**
* 带参数的访问者接口
*/
public interface ShapeVisitorWithParam<T> {
void visitCircle(Circle circle, T param);
void visitRectangle(Rectangle rectangle, T param);
void visitTriangle(Triangle triangle, T param);
}
/**
* 移动图形的访问者
*/
class MoveVisitor implements ShapeVisitorWithParam<Point> {
@Override
public void visitCircle(Circle circle, Point offset) {
// 移动圆形
Point newCenter = new Point(
circle.getCenter().getX() + offset.getX(),
circle.getCenter().getY() + offset.getY()
);
// 注意:这里需要修改Circle的状态,实际实现中可能需要setter方法
System.out.println("Moving circle from " + circle.getCenter() + " to " + newCenter);
}
@Override
public void visitRectangle(Rectangle rectangle, Point offset) {
Point newTopLeft = new Point(
rectangle.getTopLeft().getX() + offset.getX(),
rectangle.getTopLeft().getY() + offset.getY()
);
System.out.println("Moving rectangle from " + rectangle.getTopLeft() + " to " + newTopLeft);
}
@Override
public void visitTriangle(Triangle triangle, Point offset) {
// 移动三角形的三个顶点
System.out.println("Moving triangle by offset " + offset);
}
}
5.3 带上下文的访问者
java
/**
* 带执行上下文的访问者
*/
class RenderingVisitor implements ShapeVisitor {
private GraphicsContext context;
private RenderingOptions options;
public RenderingVisitor(GraphicsContext context, RenderingOptions options) {
this.context = context;
this.options = options;
}
@Override
public void visitCircle(Circle circle) {
// 使用上下文绘制圆形
context.setColor(options.getFillColor());
context.fillOval(
circle.getCenter().getX() - circle.getRadius(),
circle.getCenter().getY() - circle.getRadius(),
circle.getRadius() * 2,
circle.getRadius() * 2
);
if (options.isDrawBorder()) {
context.setColor(options.getBorderColor());
context.drawOval(
circle.getCenter().getX() - circle.getRadius(),
circle.getCenter().getY() - circle.getRadius(),
circle.getRadius() * 2,
circle.getRadius() * 2
);
}
}
@Override
public void visitRectangle(Rectangle rectangle) {
// 绘制矩形
context.setColor(options.getFillColor());
context.fillRect(
rectangle.getTopLeft().getX(),
rectangle.getTopLeft().getY(),
rectangle.getWidth(),
rectangle.getHeight()
);
if (options.isDrawBorder()) {
context.setColor(options.getBorderColor());
context.drawRect(
rectangle.getTopLeft().getX(),
rectangle.getTopLeft().getY(),
rectangle.getWidth(),
rectangle.getHeight()
);
}
}
@Override
public void visitTriangle(Triangle triangle) {
// 绘制三角形
int[] xPoints = {
(int) triangle.getPoint1().getX(),
(int) triangle.getPoint2().getX(),
(int) triangle.getPoint3().getX()
};
int[] yPoints = {
(int) triangle.getPoint1().getY(),
(int) triangle.getPoint2().getY(),
(int) triangle.getPoint3().getY()
};
context.setColor(options.getFillColor());
context.fillPolygon(xPoints, yPoints, 3);
if (options.isDrawBorder()) {
context.setColor(options.getBorderColor());
context.drawPolygon(xPoints, yPoints, 3);
}
}
}
// 上下文类
class GraphicsContext {
public void setColor(Color color) { /* ... */ }
public void fillOval(double x, double y, double width, double height) { /* ... */ }
public void drawOval(double x, double y, double width, double height) { /* ... */ }
public void fillRect(double x, double y, double width, double height) { /* ... */ }
public void drawRect(double x, double y, double width, double height) { /* ... */ }
public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { /* ... */ }
public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { /* ... */ }
}
// 渲染选项
class RenderingOptions {
private Color fillColor = Color.BLACK;
private Color borderColor = Color.BLACK;
private boolean drawBorder = true;
// getters and setters...
}
6. 访问者模式的优缺点
6.1 优点
| 优点 | 说明 |
|---|---|
| 开闭原则 | 可以在不修改现有类的情况下添加新功能 |
| 单一职责原则 | 将相关行为集中在一个访问者类中 |
| 算法集中 | 将与对象结构相关的算法集中在一个地方 |
| 状态累积 | 访问者可以在遍历过程中累积状态 |
| 类型安全 | 不需要类型检查和强制转换 |
6.2 缺点
| 缺点 | 说明 |
|---|---|
| 破坏封装 | 访问者可能需要访问元素的私有成员 |
| 元素接口变更困难 | 新增元素类型需要修改所有访问者 |
| 不适合元素频繁变化 | 如果元素类经常变化,维护成本高 |
| 可能违反迪米特法则 | 访问者需要了解元素的具体类 |
7. 访问者模式的实际应用
7.1 Java编译器中的AST访问
Java编译器使用访问者模式处理抽象语法树(AST):
java
// 简化的AST访问者模式示例
public interface ASTVisitor {
void visit(CompilationUnit node);
void visit(ClassDeclaration node);
void visit(MethodDeclaration node);
void visit(VariableDeclaration node);
void visit(Assignment node);
void visit(BinaryExpression node);
// ... 更多节点类型
}
// 类型检查访问者
public class TypeCheckingVisitor implements ASTVisitor {
private SymbolTable symbolTable = new SymbolTable();
@Override
public void visit(ClassDeclaration node) {
// 检查类声明
symbolTable.enterScope();
for (ASTNode member : node.getMembers()) {
member.accept(this);
}
symbolTable.exitScope();
}
@Override
public void visit(MethodDeclaration node) {
// 检查方法声明
symbolTable.addSymbol(node.getName(), node.getType());
node.getBody().accept(this);
}
@Override
public void visit(VariableDeclaration node) {
// 检查变量声明
symbolTable.addSymbol(node.getName(), node.getType());
}
@Override
public void visit(Assignment node) {
// 检查赋值语句的类型兼容性
node.getLeft().accept(this);
node.getRight().accept(this);
Type leftType = node.getLeft().getType();
Type rightType = node.getRight().getType();
if (!leftType.isAssignableFrom(rightType)) {
throw new TypeMismatchException("Cannot assign " + rightType + " to " + leftType);
}
}
// ... 其他visit方法
}
7.2 文件系统访问
java
/**
* 文件系统访问者
*/
public interface FileSystemVisitor {
void visit(File file);
void visit(Directory directory);
}
/**
* 文件大小计算访问者
*/
public class SizeCalculatorVisitor implements FileSystemVisitor {
private long totalSize = 0;
@Override
public void visit(File file) {
totalSize += file.getSize();
}
@Override
public void visit(Directory directory) {
// 目录本身不占空间,但需要访问其内容
for (FileSystemNode node : directory.getChildren()) {
node.accept(this);
}
}
public long getTotalSize() {
return totalSize;
}
}
/**
* 文件搜索访问者
*/
public class SearchVisitor implements FileSystemVisitor {
private String searchPattern;
private List<FileSystemNode> results = new ArrayList<>();
public SearchVisitor(String pattern) {
this.searchPattern = pattern;
}
@Override
public void visit(File file) {
if (file.getName().contains(searchPattern)) {
results.add(file);
}
}
@Override
public void visit(Directory directory) {
if (directory.getName().contains(searchPattern)) {
results.add(directory);
}
// 继续搜索子目录
for (FileSystemNode node : directory.getChildren()) {
node.accept(this);
}
}
public List<FileSystemNode> getResults() {
return results;
}
}
7.3 数据库操作
java
/**
* SQL AST访问者
*/
public interface SQLVisitor {
void visit(SelectStatement stmt);
void visit(InsertStatement stmt);
void visit(UpdateStatement stmt);
void visit(DeleteStatement stmt);
void visit(WhereClause clause);
void visit(JoinClause clause);
// ... 其他SQL元素
}
/**
* SQL格式化访问者
*/
public class SQLFormatter implements SQLVisitor {
private StringBuilder sql = new StringBuilder();
private int indentLevel = 0;
@Override
public void visit(SelectStatement stmt) {
sql.append("SELECT ");
boolean first = true;
for (Column column : stmt.getColumns()) {
if (!first) sql.append(", ");
column.accept(this);
first = false;
}
sql.append("\n").append(getIndent()).append("FROM ");
stmt.getTable().accept(this);
if (stmt.getWhereClause() != null) {
sql.append("\n").append(getIndent()).append("WHERE ");
stmt.getWhereClause().accept(this);
}
if (stmt.getOrderBy() != null) {
sql.append("\n").append(getIndent()).append("ORDER BY ");
stmt.getOrderBy().accept(this);
}
}
@Override
public void visit(WhereClause clause) {
clause.getLeft().accept(this);
sql.append(" ").append(clause.getOperator()).append(" ");
clause.getRight().accept(this);
}
// ... 其他visit方法
private String getIndent() {
return " ".repeat(indentLevel);
}
public String getFormattedSQL() {
return sql.toString();
}
}
/**
* SQL执行访问者
*/
public class SQLExecutor implements SQLVisitor {
private Connection connection;
private ResultSet resultSet;
public SQLExecutor(Connection connection) {
this.connection = connection;
}
@Override
public void visit(SelectStatement stmt) {
SQLFormatter formatter = new SQLFormatter();
stmt.accept(formatter);
String sql = formatter.getFormattedSQL();
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
// 设置参数...
resultSet = pstmt.executeQuery();
} catch (SQLException e) {
throw new RuntimeException("Failed to execute SELECT", e);
}
}
@Override
public void visit(InsertStatement stmt) {
// 执行INSERT语句
}
// ... 其他visit方法
public ResultSet getResultSet() {
return resultSet;
}
}
8. 访问者模式与其他模式的关系
8.1 与组合模式的结合
访问者模式经常与组合模式一起使用,用于遍历复杂的树形结构:
java
// 组合模式:表示部分-整体层次结构
public abstract class Component {
public abstract void accept(Visitor visitor);
public abstract void add(Component component);
public abstract void remove(Component component);
public abstract Component getChild(int index);
}
// 访问者接口
public interface Visitor {
void visit(Leaf leaf);
void visit(Composite composite);
}
// 组合访问者示例
public class PrintVisitor implements Visitor {
private int indent = 0;
@Override
public void visit(Leaf leaf) {
System.out.println(" ".repeat(indent) + "Leaf: " + leaf.getName());
}
@Override
public void visit(Composite composite) {
System.out.println(" ".repeat(indent) + "Composite: " + composite.getName());
indent++;
for (Component child : composite.getChildren()) {
child.accept(this);
}
indent--;
}
}
8.2 与迭代器模式的比较
| 模式 | 目的 | 访问控制 |
|---|---|---|
| 迭代器模式 | 提供一种顺序访问集合元素的方法 | 外部控制(客户端控制迭代) |
| 访问者模式 | 对集合中的元素执行特定操作 | 内部控制(元素控制访问) |
9. 何时使用访问者模式
9.1 适用场景
| 场景 | 说明 |
|---|---|
| 对象结构稳定 | 元素类很少变化,但经常需要添加新操作 |
| 多个不相关操作 | 需要对对象结构执行多种不同且不相关的操作 |
| 避免污染元素类 | 不想让操作代码污染元素类的代码 |
| 遍历复杂结构 | 需要遍历复杂的对象结构,并对每个元素执行操作 |
| 跨类层次操作 | 操作需要跨越多个类的层次结构 |
9.2 不适用场景
| 场景 | 说明 |
|---|---|
| 元素类频繁变化 | 如果经常添加新的元素类,需要修改所有访问者 |
| 元素接口不稳定 | 如果元素的接口经常变化,访问者模式不适用 |
| 元素封装重要 | 如果不想暴露元素的内部细节给访问者 |
| 简单操作 | 如果只有少数简单操作,访问者模式可能过度设计 |
10. 访问者模式的最佳实践
10.1 设计建议
- 最小化访问者的职责:每个访问者应该只负责一个特定的操作
- 使用访问者状态:访问者可以在遍历过程中累积状态
- 考虑空访问者:为不需要处理所有元素类型的访问者提供默认实现
- 访问者组合:可以组合多个访问者完成复杂操作
- 访问者工厂:使用工厂模式创建访问者实例
10.2 性能考虑
java
/**
* 性能优化的访问者模式
*/
public class OptimizedVisitorPattern {
// 使用缓存存储访问者实例
private static final Map<Class<?>, Map<Class<?>, BiConsumer<Object, Object>>>
DISPATCH_CACHE = new ConcurrentHashMap<>();
// 双重分派优化
public static <T, R> R accept(T element, Visitor<T, R> visitor) {
Class<?> elementClass = element.getClass();
Class<?> visitorClass = visitor.getClass();
BiConsumer<Object, Object> dispatcher = DISPATCH_CACHE
.computeIfAbsent(elementClass, k -> new ConcurrentHashMap<>())
.computeIfAbsent(visitorClass, k -> createDispatcher(elementClass, visitorClass));
// 使用预编译的分派逻辑
// ...
return null;
}
private static BiConsumer<Object, Object> createDispatcher(
Class<?> elementClass, Class<?> visitorClass) {
// 使用反射或字节码生成创建高效的分派逻辑
// ...
return null;
}
}
10.3 测试访问者
java
/**
* 访问者模式的测试
*/
public class VisitorPatternTest {
@Test
public void testAreaCalculator() {
// 准备测试数据
ShapeCollection shapes = new ShapeCollection();
shapes.addShape(new Circle(5.0, new Point(0, 0)));
shapes.addShape(new Rectangle(4.0, 3.0, new Point(0, 0)));
// 创建访问者
AreaCalculator calculator = new AreaCalculator();
// 执行测试
shapes.accept(calculator);
// 验证结果
double expectedArea = Math.PI * 25 + 12; // 圆面积 + 矩形面积
assertEquals(expectedArea, calculator.getTotalArea(), 0.001);
}
@Test
public void testXmlExporter() {
// 准备测试数据
ShapeCollection shapes = new ShapeCollection();
shapes.addShape(new Circle(5.0, new Point(0, 0)));
// 创建访问者
XMLExporter exporter = new XMLExporter();
// 执行测试
shapes.accept(exporter);
String xml = exporter.getXML();
// 验证结果
assertTrue(xml.contains("<circle>"));
assertTrue(xml.contains("<radius>5.0</radius>"));
assertTrue(xml.contains("</shapes>"));
}
@Test
public void testCompositeVisitor() {
// 测试组合结构和访问者
Composite root = new Composite("root");
root.add(new Leaf("leaf1"));
root.add(new Leaf("leaf2"));
Composite child = new Composite("child");
child.add(new Leaf("leaf3"));
root.add(child);
PrintVisitor printer = new PrintVisitor();
root.accept(printer);
// 验证输出
// ...
}
}
11. 总结
访问者模式是一种强大的行为设计模式,它允许你在不修改现有类的情况下为它们添加新的操作。通过将算法与对象结构分离,访问者模式提供了以下关键优势:
11.1 核心价值
- 开闭原则的典范:对扩展开放,对修改关闭
- 算法集中化:将与对象结构相关的算法集中在一个地方
- 类型安全:避免了类型检查和强制转换
- 状态累积:访问者可以在遍历过程中累积状态
11.2 关键要点
- 双重分派是访问者模式的核心机制
- 访问者模式在对象结构稳定但操作频繁变化的场景中特别有用
- 访问者模式可能会破坏封装,因为它需要访问元素的内部状态
- 访问者模式与组合模式 、迭代器模式有很好的协同效应
11.3 使用建议
- 当需要对一个复杂对象结构执行多种不相关的操作时,考虑使用访问者模式
- 如果对象结构频繁变化,访问者模式可能不是最佳选择
- 考虑使用访问者模式来实现编译器、解释器、文件系统工具等
- 在需要跨多个类层次执行操作时,访问者模式提供了一种优雅的解决方案
访问者模式是设计模式中比较复杂的一种,但它为解决一类特定问题提供了优雅的解决方案。当你需要在不修改现有类的情况下为它们添加新功能时,访问者模式是一个值得考虑的选项。