设计模式-访问者模式详解

访问者模式:在不修改类的情况下扩展功能

      1. 问题场景:图形处理系统的挑战
      • 1.1 传统做法的问题
      1. 访问者模式:优雅的解决方案
      • 2.1 访问者模式的核心思想
      1. 访问者模式的完整实现
      • 3.1 第一步:定义可访问的元素接口
      • 3.2 第二步:重构具体元素类
      • 3.3 第三步:定义访问者接口
      • 3.4 第四步:实现具体访问者
      • 3.5 第五步:图形集合类
      • 3.6 第六步:客户端使用示例
      1. 访问者模式的核心组件
      • 4.1 双重分派(Double Dispatch)
      • 4.2 访问者模式类图
      1. 访问者模式的变体
      • 5.1 带返回值的访问者
      • 5.2 带参数的访问者
      • 5.3 带上下文的访问者
      1. 访问者模式的优缺点
      • 6.1 优点
      • 6.2 缺点
      1. 访问者模式的实际应用
      • 7.1 Java编译器中的AST访问
      • 7.2 文件系统访问
      • 7.3 数据库操作
      1. 访问者模式与其他模式的关系
      • 8.1 与组合模式的结合
      • 8.2 与迭代器模式的比较
      1. 何时使用访问者模式
      • 9.1 适用场景
      • 9.2 不适用场景
      1. 访问者模式的最佳实践
      • 10.1 设计建议
      • 10.2 性能考虑
      • 10.3 测试访问者
      1. 总结
      • 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");
    }
}

现在,我们需要为这些图形添加新功能:

  1. 计算面积
  2. 导出为XML
  3. 碰撞检测
  4. 序列化为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();
}

问题

  1. 每次添加新功能,都需要修改所有Shape子类
  2. 违反了开闭原则(对扩展开放,对修改关闭)
  3. 如果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));
    }
}

问题

  1. 类型检查和强制转换
  2. 违反开闭原则,新增图形类型需要修改所有方法
  3. 逻辑分散,难以维护

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

访问者模式包含两个核心层次结构:

  1. 元素层次结构:包含需要被访问的类
  2. 访问者层次结构:包含对元素进行操作的类

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)

访问者模式的关键是双重分派机制:

  1. 第一次分派 :客户端调用元素的accept(visitor)方法
  2. 第二次分派 :元素调用访问者的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 设计建议

  1. 最小化访问者的职责:每个访问者应该只负责一个特定的操作
  2. 使用访问者状态:访问者可以在遍历过程中累积状态
  3. 考虑空访问者:为不需要处理所有元素类型的访问者提供默认实现
  4. 访问者组合:可以组合多个访问者完成复杂操作
  5. 访问者工厂:使用工厂模式创建访问者实例

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 核心价值

  1. 开闭原则的典范:对扩展开放,对修改关闭
  2. 算法集中化:将与对象结构相关的算法集中在一个地方
  3. 类型安全:避免了类型检查和强制转换
  4. 状态累积:访问者可以在遍历过程中累积状态

11.2 关键要点

  1. 双重分派是访问者模式的核心机制
  2. 访问者模式在对象结构稳定但操作频繁变化的场景中特别有用
  3. 访问者模式可能会破坏封装,因为它需要访问元素的内部状态
  4. 访问者模式与组合模式迭代器模式有很好的协同效应

11.3 使用建议

  • 当需要对一个复杂对象结构执行多种不相关的操作时,考虑使用访问者模式
  • 如果对象结构频繁变化,访问者模式可能不是最佳选择
  • 考虑使用访问者模式来实现编译器、解释器、文件系统工具等
  • 在需要跨多个类层次执行操作时,访问者模式提供了一种优雅的解决方案

访问者模式是设计模式中比较复杂的一种,但它为解决一类特定问题提供了优雅的解决方案。当你需要在不修改现有类的情况下为它们添加新功能时,访问者模式是一个值得考虑的选项。

相关推荐
Yu_Lijing2 小时前
基于C++的《Head First设计模式》笔记——组合模式
c++·笔记·设计模式·组合模式
Engineer邓祥浩2 小时前
设计模式学习(17) 23-15 访问者模式
学习·设计模式·访问者模式
weixin_462446232 小时前
使用 pip3 一键卸载当前环境中所有已安装的 Python 包(Linux / macOS / Windows)
linux·python·macos
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 收藏功能实现
android·java·开发语言·javascript·python·flutter·游戏
C++实习生2 小时前
Visual Studio 2017 Enterprise 组件目录
后端·python·flask
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 个人中心实现
android·java·javascript·python·flutter·游戏
摘星编程2 小时前
OpenHarmony + RN:decay滚动惯性动画实现
python
SunnyRivers2 小时前
如何将基于 setup.py 的项目现代化?
python·setup
AI_567810 小时前
Selenium+Python可通过 元素定位→操作模拟→断言验证 三步实现Web自动化测试
服务器·人工智能·python