设计模式-访问者(Visitor)模式详解和应用

文章目录


前言

最近在做一个根据数学表达式生成java执行代码的功能,其中用到了访问者模式。使我对访问者模式有了更深入的理解。故写下此篇文章分享出来,不足之处请大家指正。


访问者模式介绍

访问者模式(Visitor Pattern)是GoF提出的23种设计模式中的一种,属于行为模式。GoF《Design Pattern》中的定义 :表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。

结构

包含的角色

  • 抽象访问者(Visitor)。定义了访问者可以访问的元素对象的接口,包含多个 visit() 方法,每个方法对应一个具体元素对象。
  • 具体访问者(ConcreteVisitor)。实现了抽象访问者接口,对不同类型的元素对象进行具体的操作。
  • 抽象元素(Element)。定义了接受访问者accept方法,不同的具体元素可以实现该方法以不同的方式响应访问者的访问。
  • 具体元素(ConcreteElement)。实现了抽象元素中定义的接受访问者访问的方法,并在其中调用访问者的访问方法。
  • 对象结构(Object Structure)。定义了具体元素的集合,并提供了遍历集合中元素的方法。

应用场景

  1. 电商网站的商品分类与操作

    • 电商网站通常有大量的商品,这些商品可以按照不同的属性进行分类,如数码产品、服装鞋帽、家居用品等。
    • 使用访问者模式,可以定义一个访问者对象,它能够访问不同类型的商品对象,并根据需要执行不同的操作,如按照价格排序、根据品牌筛选等。
  2. 图形编辑器的操作处理

    • 在一个复杂的图形编辑器中,有多种图形元素,如线条、圆形、矩形等。
    • 访问者模式允许定义一个访问者对象,它能够访问这些图形元素并执行不同的操作,如移动、缩放、旋转或改变颜色等。
  3. 编译器和解释器设计

    • 在编译器或解释器的设计中,抽象语法树(AST)是一个常见的数据结构,它包含了程序的各个部分。
    • 使用访问者模式,可以为AST中的不同节点类型定义不同的访问者,以执行如类型检查、代码优化、代码生成等操作。

代码示例

将Visitor设计模式与Shape类结合使用,你可以定义一个Shape接口和多个具体的Shape实现类(如Circle、Rectangle、Triangle等),并为这些Shape对象实现不同的操作。

以下是一个简单的示例,展示了如何使用Visitor设计模式对Shape对象进行操作:

java 复制代码
// Shape 接口
interface Shape {  
    void accept(ShapeOperation operation);  
}  
  
// Circle 类,实现了 Shape 接口  
class Circle implements Shape {  
    private double radius;  
  
    public Circle(double radius) {  
        this.radius = radius;  
    }  
  
    public double getRadius() {  
        return radius;  
    }  
  
    @Override  
    public void accept(ShapeOperation operation) {  
        operation.visit(this);  
    }  
}  
  
// Square 类,实现了 Shape 接口  
class Square implements Shape {  
    private double side;  
  
    public Square(double side) {  
        this.side = side;  
    }  
  
    public double getSide() {  
        return side;  
    }  
  
    @Override  
    public void accept(ShapeOperation operation) {  
        operation.visit(this);  
    }  
}

定义一个ShapeVisitor接口和具体的Visitor实现类:

java 复制代码
// ShapeVisitor 接口
interface ShapeVisitor {  
    void visit(Circle circle);  
    void visit(Square square);  
}  
  
// PrintAreaVisitor 类,实现了 ShapeOperation 接口  
class PrintAreaVisitor implements ShapeVisitor {  
    @Override  
    public void visit(Circle circle) {  
        double area = Math.PI * circle.getRadius() * circle.getRadius();  
        System.out.println("Circle area: " + area);  
    }  
  
    @Override  
    public void visit(Square square) {  
        double area = square.getSide() * square.getSide();  
        System.out.println("Square area: " + area);  
    }  
}  
  
// PrintPerimeterVisitor 类,实现了 ShapeOperation 接口  
class PrintPerimeterVisitor implements ShapeVisitor {  
    @Override  
    public void visit(Circle circle) {  
        double perimeter = 2 * Math.PI * circle.getRadius();  
        System.out.println("Circle perimeter: " + perimeter);  
    }  
  
    @Override  
    public void visit(Square square) {  
        double perimeter = 4 * square.getSide();  
        System.out.println("Square perimeter: " + perimeter);  
    }  
}

使用Visitor对Shape对象进行操作:

java 复制代码
public class ShapeClient {
    public static void main(String[] args) {  
        Shape circle = new Circle(5);  
        Shape square = new Square(10);  
  
        // 使用 PrintAreaVisitor 来打印面积  
        ShapeVisitor areaVisitor = new PrintAreaVisitor();  
        circle.accept(areaVisitor);  
        square.accept(areaVisitor);  
  
        // 使用 PrintPerimeterVisitor 来打印周长  
        ShapeVisitor perimeterVisitor = new PrintPerimeterVisitor();  
        circle.accept(perimeterVisitor);  
        square.accept(perimeterVisitor);  
    }  
}

访问者模式的扩展

访问者(Visitor)模式常常同以下设计模式联用:

  • 与"迭代器模式"联用。因为访问者模式中的"对象结构"是一个包含元素角色的容器,当访问者遍历容器中的所有元素时,常常要用迭代器。如上面示例代码中的对象结构是用 List 实现的,它通过 List 对象的 Iterator() 方法获取迭代器。如果对象结构中的聚合类没有提供迭代器,也可以用迭代器模式自定义一个。

  • 访问者(Visitor)模式与"组合模式"联用。因为访问者(Visitor)模式中的"元素对象"可能是叶子对象或者是容器对象,如果元素对象包含容器对象,就必须用到组合模式。

  • 与模板方法模式联用:可以在保持算法骨架不变的情况下,通过访问者模式为不同的对象结构添加新的操作。具体来说,可以将模板方法模式中定义的一系列步骤作为访问者接口中的方法,然后在具体的访问者实现中定义这些步骤的具体行为。这样,就可以通过创建不同的访问者来实现对同一对象结构的不同操作。

    假设我们有一个系统需要处理不同类型的文件(如文本文件、图像文件等),每种文件类型都有一系列的处理步骤(如打开、读取、处理、关闭等)。我们可以使用模板方法模式来定义这些处理步骤的算法骨架,并使用访问者模式来为每个文件类型定义具体的处理逻辑。

    首先,我们定义一个处理文件的算法骨架(模板方法模式):

java 复制代码
public abstract class FileProcessor {  
    public final void processFile(File file) {  
        openFile(file);  
        readFile(file);  
        handleFile(file); // 这是一个钩子方法,可以在子类中覆盖  
        closeFile(file);  
    }  
      
    protected abstract void openFile(File file);  
    protected abstract void readFile(File file);  
    protected void handleFile(File file) {  
        // 默认实现,可以在子类中覆盖  
    }  
    protected abstract void closeFile(File file);  
}

然后,我们定义访问者接口,该接口包含与文件处理步骤相对应的方法:

java 复制代码
public interface FileVisitor {  
    void visitOpenFile(File file);  
    void visitReadFile(File file);  
    void visitHandleFile(File file);  
    void visitCloseFile(File file);  
}

接着,我们为每种文件类型创建具体的访问者实现:

java 复制代码
public class TextFileVisitor implements FileVisitor {  
    @Override  
    public void visitOpenFile(File file) {  
        // 文本文件特定的打开逻辑  
    }  
      
    @Override  
    public void visitReadFile(File file) {  
        // 文本文件特定的读取逻辑  
    }  
      
    @Override  
    public void visitHandleFile(File file) {  
        // 文本文件特定的处理逻辑  
    }  
      
    @Override  
    public void visitCloseFile(File file) {  
        // 文本文件特定的关闭逻辑  
    }  
}  
  
// 类似地,可以为图像文件等创建其他的FileVisitor实现...

最后,我们创建具体的文件处理器类,并将访问者注入其中:

java 复制代码
public class TextFileProcessor extends FileProcessor {  
    private FileVisitor visitor;  
      
    public TextFileProcessor(FileVisitor visitor) {  
        this.visitor = visitor;  
    }  
      
    @Override  
    protected void openFile(File file) {  
        visitor.visitOpenFile(file);  
    }  
      
    @Override  
    protected void readFile(File file) {  
        visitor.visitReadFile(file);  
    }  
      
    @Override  
    protected void handleFile(File file) {  
        visitor.visitHandleFile(file);  
    }  
      
    @Override  
    protected void closeFile(File file) {  
        visitor.visitCloseFile(file);  
    }  
}

访问者模式优缺点

访问者模式是一种将数据操作与数据结构分离的设计模式,它使得你可以在不修改已有类的情况下增加新的操作。以下是访问者模式的优点和缺点:

优点:

  1. 扩展性好:访问者模式使得增加新的操作变得容易。当需要增加新的操作时,只需要增加新的访问者类即可,无需修改已有的元素类。
  2. 符合单一职责原则:访问者模式可以将原本分散在各个元素类中的操作集中起来,使得每个类的职责更加单一。
  3. 灵活性高:访问者模式可以灵活地访问和操作对象结构中的元素,通过访问者接口可以定义多种不同的访问和操作方式。
  4. 双层分离:访问者模式将数据与数据结构分离,使得两者可以独立变化,降低了系统的耦合度。

缺点:

  1. 增加系统复杂性:访问者模式增加了新的抽象层次,可能导致系统变得更加复杂。如果系统本身并不复杂,或者不需要经常增加新的操作,使用访问者模式可能会引入不必要的复杂性。
  2. 破坏封装性:访问者模式要求被访问的对象暴露一些方法和数据,这可能会破坏对象的封装性。
  3. 增加新的元素变更困难:对于访问者而言,增加新的元素类很困难,因为访问者类针对每一个元素类都提供访问操作,因此在系统中增加新的元素类,就必须修改所有访问者类,增加新的操作。这违背了"开闭原则"。
  4. 违反了依赖倒置原则:访问者模式依赖了具体类,而没有依赖抽象类。

总的来说,访问者模式在某些场景下非常有用,但在选择是否使用它时,需要权衡其优缺点,并结合具体的应用场景来决定。

总结

访问者模式主要用于将操作与数据结构分离,增加新的操作变得简单,但增加新的元素类可能会导致修改访问者接口和所有的具体访问者类。以下情况可以考虑使用访问者模式:

  1. 对象结构稳定但操作易变:当有一个对象结构(如集合、数组等),其包含的元素类型比较稳定,但针对这些元素的操作经常发生变化时,Visitor模式非常适用。使用Visitor模式,你可以在不修改对象结构的情况下,通过添加新的访问者类来实现新的操作。

  2. 避免使用多重继承:在一些编程语言中,多重继承可能会引入复杂性、潜在的冲突和不易维护的代码。使用Visitor模式,你可以将操作逻辑封装在访问者类中,从而避免使用多重继承。

  3. 跨多个类进行类似操作:当需要在多个不相关的类中执行类似的操作时,Visitor模式可以帮助你将这些操作集中管理。通过将操作逻辑放在访问者类中,你可以减少代码重复,提高代码的可维护性。

  4. 操作需要访问对象内部状态:如果操作需要访问对象的内部状态,并且这些状态对于操作来说是透明的,那么使用Visitor模式可以将操作与对象状态分离。这样,你可以在不修改对象类的情况下,通过修改访问者类来改变操作的行为。

相关推荐
是小胡嘛14 分钟前
数据结构之旅:红黑树如何驱动 Set 和 Map
数据结构·算法
m0_7482550218 分钟前
前端常用算法集合
前端·算法
呆呆的猫41 分钟前
【LeetCode】227、基本计算器 II
算法·leetcode·职场和发展
Tisfy42 分钟前
LeetCode 1705.吃苹果的最大数目:贪心(优先队列) - 清晰题解
算法·leetcode·优先队列·贪心·
余额不足121381 小时前
C语言基础十六:枚举、c语言中文件的读写操作
linux·c语言·算法
火星机器人life3 小时前
基于ceres优化的3d激光雷达开源算法
算法·3d
虽千万人 吾往矣3 小时前
golang LeetCode 热题 100(动态规划)-更新中
算法·leetcode·动态规划
arnold664 小时前
华为OD E卷(100分)34-转盘寿司
算法·华为od
ZZTC5 小时前
Floyd算法及其扩展应用
算法
lshzdq5 小时前
【机器人】机械臂轨迹和转矩控制对比
人工智能·算法·机器人