简介

访问者模式是一种行为型设计模式,它可以用于在不修改已有对象结构的情况下,定义新的操作方式。简单地说就是在不改变数据结构的前提下,通过在数据结构中加入一个新的角色——访问者,来达到执行不同操作的目的。

模式结构

  • 抽象访问者 Visitor:定义了对自身数据结构中各个元素的操作,为每个具体元素类对应一个访问操作,该操作中的参数标识了被访问的具体元素。

  • 具体访问者 Concrete Visitor:实现了访问者接口中定义的具体操作,确定访问者访问一个元素时该做什么。

  • 抽象元素 Element:定义了接受访问者的接口,通常是一个接口或抽象类,其中定义了一个接受访问者的方法,被访问者对象作为方法的参数。

  • 具体元素 Concrete Element:实现了抽象元素接口,它是数据结构中的具体的元素,用于接收具体的访问者并执行相应的操作。每个具体元素都有自己的业务逻辑,并在接收访问者时将具体的操作委托给访问者进行处理。

  • 对象结构 Object Structure:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。

优缺点

优点

  • 扩展性好:能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  • 复用性好:可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。

  • 灵活性好:访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。

  • 符合单一职责原则:访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

缺点

  • 违反开闭原则:在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  • 违反迪米特法则:在访问者模式中,具体元素类需要暴露接受访问者的方法给访问者使用,这样破坏了具体元素类对于访问者的封装性,增加了元素类与具体访问者之间的耦合性。

  • 破坏封装性:访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。

应用场景

生活场景

  • 蔬菜摊位:在一个农贸市场的蔬菜摊位上,摊主可能提供不同的服务,如称重、清洗、切割等。每个服务可以被视为一个访问者,而蔬菜则是元素,根据不同的服务访问者进行相应的操作。

  • 医院就诊:在医院就诊时,医生、护士和药师可以视为不同的访问者,而病人则是元素。不同的访问者根据其职责和需要,对病人进行不同的操作和处理,如诊断、治疗、开药等。

  • 旅游参观:游客可以参观不同的地方,如参观文物、欣赏风景等。游客可以看作访问者,而景点本身则是元素,根据游客的选择展示不同的参观操作。

程序场景

  • XML解析器:许多Java XML解析器,如DOM和SAX解析器,使用访问者模式来处理XML文档。XML的节点可以被视为元素,而访问者则可以是处理XML节点的具体操作,如提取数据、修改节点等。

  • 数据库访问:一些Java数据库访问框架,如Hibernate和MyBatis,使用访问者模式来处理数据库操作。数据库表和字段可以被视为元素,而访问者则可以是查询、更新或删除操作,通过访问者模式来执行这些操作。

  • 文件系统操作:Java的文件和目录操作中,使用了访问者模式。Java提供了FileVisitor接口和SimpleFileVisitor类,可以通过自定义的访问者类来遍历文件系统,并执行不同的操作,如复制文件、删除文件等。

示例演示

下面以“旅游参观”为例,解释一下访问者模式。游客挑选不同的景点来参观,如参观文物、欣赏风景等。那么游客可以看作访问者,而景点本身则是元素,根据游客的选择展示不同的参观操作。

UML类图

抽象访问者Visitor

/**
 * 抽象访问者(Visitor):参观者
 * 定义:定义了对自身数据结构中各个元素的操作,为每个具体元素类对应一个访问操作,该操作中的参数标识了被访问的具体元素。
 */
public interface Visitor {
    void visit(Relic relic);
    void visit(View scenery);
}

具体访问者Tourist

/**
 * 具体访问者(Concrete Visitor):游客
 * 定义:实现了访问者接口中定义的具体操作,确定访问者访问一个元素时该做什么。
 */
public class Tourist implements Visitor{
    @Override
    public void visit(Relic relic) {
        System.out.println("游客正在参观文物...");
        relic.display();
    }
 
    @Override
    public void visit(View scenery) {
        System.out.println("游客正在欣赏自然风景...");
        scenery.display();
    }
}

抽象元素Spot

/**
 * 抽象元素(Element):景点
 * 定义:定义了接受访问者的接口,通常是一个接口或抽象类,其中定义了一个接受访问者的方法,被访问者对象作为方法的参数。
 */
public interface Spot {
    void accept(Visitor visitor);
}

具体元素View、Relic

/**
 * 具体元素(Concrete Element):风景
 * 定义:实现了抽象元素接口,它是数据结构中的具体的元素,用于接收具体的访问者并执行相应的操作。
 * 每个具体元素都有自己的业务逻辑,并在接收访问者时将具体的操作委托给访问者进行处理。
 */
public class View implements Spot{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this); // 将自身传递给访问者
    }
    // 具体自然风景的业务逻辑
    public void display() {
        System.out.println("这是一处壮丽的自然风景。");
    }
}
​
/**
 * 具体元素(Concrete Element):文物
 * 定义:实现了抽象元素接口,它是数据结构中的具体的元素,用于接收具体的访问者并执行相应的操作。
 * 每个具体元素都有自己的业务逻辑,并在接收访问者时将具体的操作委托给访问者进行处理。
 */
public class Relic implements Spot{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this); // 将自身传递给访问者
    }
    // 具体文物的业务逻辑
    public void display() {
        System.out.println("这是一件珍贵的文物。");
    }
}

对象结构SpotCollection

/**
 * 对象结构(Object Structure)
 * 定义:包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。
 */
public class SpotCollection {
    private List<Spot> spots = new ArrayList<>();
 
    //添加元素
    public void addSpot(Spot spot) {
        spots.add(spot);
    }
 
    public void accept(Visitor visitor) {
        for (Spot spot : spots) {
            spot.accept(visitor);
        }
    }
}

测试

/**
 * 访问者模式测试类
 */
@SpringBootTest
public class TestVisitor {
 
    @Test
    void testVisitor(){
        //创建对象结构
        SpotCollection spotCollection = new SpotCollection();
        //添加元素
        spotCollection.addSpot(new Relic());
        spotCollection.addSpot(new View());
 
        Tourist tourist = new Tourist();
        //景点接受游客的访问
        spotCollection.accept(tourist);
    }
}

测试结果

游客正在参观文物...
这是一件珍贵的文物。
游客正在欣赏自然风景...
这是一处壮丽的自然风景。

总结

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

以下情况,可以考虑使用访问者模式:

  • 当对象结构中的元素类(Element)和操作类(Operation)的种类固定或者很少改变,但是需要经常新增或者修改操作时。

  • 当需要对一个对象结构中的元素进行多种不同且无关的操作时。

  • 当需要对不同类型的元素对象进行某种共同的处理逻辑时。

  • 当对象结构中的元素类(Element)和操作类(Operation)的种类很多,并且相互之间的组合和嵌套可能会产生大量的代码重复时。

  • 当需要在不修改元素对象的类结构的前提下,给元素对象新增一些操作时。