设计模式-访问者模式

发布时间:2023年12月18日

访问者模式是设计模式中行为型模式的一种(其他的还有如创建型、结构型),听说是设计模式中比较难理解的一种,最近项目中用到了该模式,所以今天总结和实践一下。

一、访问者模式要解决的问题:

稳定的数据结构和易变的操作耦合问题

二、动手实现访问者模式Demo

这里使用学校体育馆和访客的例子,体育馆里有一些场馆如羽毛球馆、篮球馆、乒乓球馆,学生可以到体育馆进行打球。

1. 不使用设计模式常规实现

先演示下没有使用访问者模式的实现,首先,学校体育馆抽象类如下:

public abstract class Gymnasium {
    protected String people;
    public Gymnasium(String people) {
        this.people = people;
    }
}

然后,几种体育场馆的实现类:

/**
 * 羽毛球馆
 */
public class BadmintonHall extends Gymnasium{
    public BadmintonHall(String people) {
        super(people);
    }

    @Override
    protected void playBall() {
        System.out.println(">>>>>>"+people+"在打羽毛球");
    }
}
/**
 * 篮球场馆
 */
public class BasketballCourt extends Gymnasium{

    public BasketballCourt(String people) {
        super(people);
    }

    @Override
    protected void playBall() {
        System.out.println(">>>>>>"+people+"在打篮球");
    }
}
/**
 * 乒乓球场馆
 */
public class TableTennisHall extends Gymnasium{

    public TableTennisHall(String people) {
        super(people);
    }

    @Override
    protected void playBall() {
        System.out.println(">>>>>>"+people+"在打乒乓球");
    }
}

测试结果

public class ClientTest {
    public static void main(String[] args) {
        List<Gymnasium> gymnasiums=new ArrayList<>();
        gymnasiums.add(new BadmintonHall("小张"));
        gymnasiums.add(new BasketballCourt("小王"));
        gymnasiums.add(new BasketballCourt("小亮"));
        gymnasiums.add(new TableTennisHall("小崔"));

        for (Gymnasium gymnasium : gymnasiums) {
            gymnasium.playBall();
        }
    }
}
// 运行结果:
>>>>>>小张在打羽毛球
>>>>>>小王在打篮球
>>>>>>小亮在打篮球
>>>>>>小崔在打乒乓球

这样就实现了我们想要的功能,试想一种情况,学校里的体育馆有时候不仅让我们课下打球,有时还需要上体育课,这个时候,每个体育场馆实现类都要增加一个上体育课的方法,这样其实违背了开闭原则,随着功能的不断增加,每个类代码会不断增多,如果我们不想频繁修改原有类,就可以按照访问者设计模式把业务操作提取到外边,访问者模式的类的基本结构如下:

  • 抽象元素(Element),如体育场馆,包含了一个accept()接口
  • 具体元素(ConcreteElement),具体的体育场馆,如篮球场馆,乒乓球场馆,实现抽象元素的accept()方法
  • 抽象访问者(Visitor),为每一个具体元素定义一个visit操作方法
  • 具体访问者(ConcreteVisitor),也就是抽取出来的方法,指明了访问者访问时具体的操作内容
  • 对象结构(ObjectStructure),一个集合,用于存放元素对象,提供让访问者遍历内部元素的方法,通常由List、Set、Map等实现。
2. 访问者模式实现

访问者接口及其实现类如下:

public interface Visitor {
    void visit(BadmintonHall badmintonHall);
    void visit(BasketballCourt basketballCourt);
    void visit(TableTennisHall tableTennisHall);
}

public class PlayBallVisitor implements Visitor{
    @Override
    public void visit(BadmintonHall badmintonHall) {
        System.out.println(">>>>>>"+badmintonHall.people+"在打羽毛球");
    }

    @Override
    public void visit(BasketballCourt basketballCourt) {
        System.out.println(">>>>>>"+basketballCourt.people+"在打篮球");
    }

    @Override
    public void visit(TableTennisHall tableTennisHall) {
        System.out.println(">>>>>>"+tableTennisHall.people+"在打乒乓球");
    }
}

学校体育馆及其实现类

public abstract class Gymnasium {

    protected String people;

    public Gymnasium(String people) {
        this.people = people;
    }

    abstract void accept(Visitor visitor);
}
/**
 * 羽毛球馆
 */
public class BadmintonHall extends Gymnasium{
    public BadmintonHall(String people) {
        super(people);
    }

    @Override
    void accept(Visitor visitor) {
        visitor.visit(this);
    }
}
/**
 * 篮球场
 */
public class BasketballCourt extends Gymnasium{

    public BasketballCourt(String people) {
        super(people);
    }

    @Override
    void accept(Visitor visitor) {
        visitor.visit(this);
    }
}
/**
 * 乒乓球场馆
 */
public class TableTennisHall extends Gymnasium{

    public TableTennisHall(String people) {
        super(people);
    }
    @Override
    void accept(Visitor visitor) {
        visitor.visit(this);
    }

}

客户端测试,运行结果和之前相同

public class Client {
    public static void main(String[] args) {
        List<Gymnasium> gymnasiums=new ArrayList<>();
        gymnasiums.add(new BadmintonHall("小张"));
        gymnasiums.add(new BasketballCourt("小王"));
        gymnasiums.add(new BasketballCourt("小亮"));
        gymnasiums.add(new TableTennisHall("小崔"));

        PlayBallVisitor playBallVisitor=new PlayBallVisitor();
        for (Gymnasium gymnasium : gymnasiums) {
            gymnasium.accept(playBallVisitor);
        }
    }
}
// 运行结果如下:
>>>>>>小张在打羽毛球
>>>>>>小王在打篮球
>>>>>>小亮在打篮球
>>>>>>小崔在打乒乓球
3.试想一次扩展

假如新增一个上体育课的操作方法,此时只需要增加一个Visitor的实现类即可

public class PEVisitor implements Visitor{
    @Override
    public void visit(BadmintonHall badmintonHall) {
        System.out.println(">>>>>>"+badmintonHall.people+"在羽毛球馆教羽毛球课");
    }

    @Override
    public void visit(BasketballCourt basketballCourt) {
        System.out.println(">>>>>>"+basketballCourt.people+"在篮球馆指导学生练习篮球");
    }

    @Override
    public void visit(TableTennisHall tableTennisHall) {
        System.out.println(">>>>>>"+tableTennisHall.people+"在乒乓球馆指导学生练习乒乓球");
    }
}

测试结果如下:

public class Client {
    public static void main(String[] args) {
        List<Gymnasium> stuGymnasiums=new ArrayList<>();
        stuGymnasiums.add(new BadmintonHall("小张"));
        stuGymnasiums.add(new BasketballCourt("小王"));
        stuGymnasiums.add(new BasketballCourt("小亮"));
        stuGymnasiums.add(new TableTennisHall("小崔"));

        List<Gymnasium> gymnasiums=new ArrayList<>();
        gymnasiums.add(new BadmintonHall("张老师"));
        gymnasiums.add(new BasketballCourt("王老师"));
        gymnasiums.add(new TableTennisHall("李老师"));

        PlayBallVisitor playBallVisitor=new PlayBallVisitor();
        for (Gymnasium stuGymnasium : stuGymnasiums) {
            stuGymnasium.accept(playBallVisitor);
        }
        PEVisitor peVisitor=new PEVisitor();
        for (Gymnasium gymnasium : gymnasiums) {
            gymnasium.accept(peVisitor);
        }
    }
}
// 运行结果
>>>>>>小张在打羽毛球
>>>>>>小王在打篮球
>>>>>>小亮在打篮球
>>>>>>小崔在打乒乓球
>>>>>>张老师在羽毛球馆教羽毛球课
>>>>>>王老师在篮球馆指导学生练习篮球
>>>>>>李老师在乒乓球馆指导学生练习乒乓球

可以看到,访问者模式实现的关键是对Vistior类的抽取,以及每个元素Element有个关键的方法accept(),来接受Visitor的访问,accept方法在调用 visitor.visit(this)时候,就可以拿到当前具体元素的信息进行操作了。上面例子的类图如下:
在这里插入图片描述

三、访问者模式的应用场景

我们需要对一组对象进行一些业务操作,但为了避免不断的在对象中添加功能“污染”到原来的对象,导致类越来越臃肿,职责不够单一,可以使用访问者模式,将对象新增加的业务操作抽离出来,放在具体的访问者接口和实现类中。

实际应用中,如复杂的嵌套结构访问就可以使用访问者模式,如对文件系统的遍历,文件系统是一个树状结构,包含文件和文件夹等元素。使用访问者模式可以设计一个访问者,用于执行不同的操作,比如计算文件夹大小、统计文件数量等。如java.nio.file包中的FileVistor类,就用到了访问者模式。

四、优缺点

优点
  • 符合单一职责原则;
  • 新增新访问操作,只需要实现一个访问类,符合开闭原则;
  • 灵活、易扩展;
缺点
  • 代码可读性差,不了解该模式,则难以理解;
  • 新增新元素的时候,需要修改抽象访问者和具体访问者实现类,违背开闭原则;
  • 访问某个元素时,可能没有访问元素私有成员变量和方法的必要权限

五、访问者模式中的伪动态双分派概念

访问者模式中的伪动态双分派,是指在执行操作时,根据两个元素的类型动态地选择执行哪个方法。这个"伪动态双分派"是因为在很多编程语言中,并没有真正支持双分派(double dispatch),而是通过一些技巧来实现类似的效果。上面代码中我们元素实现了

@Override
    void accept(Visitor visitor) {
        visitor.visit(this);
    }

这里的伪动态双分派体现在accept方法的调用上。第一次单分派,是执行的哪一个元素的accept方法,第二次分派是执行的哪个visitor的visit方法。根据元素类型和访问者类型的动态选择方法的调用,从而达到双分派的效果。

分派的概念,就是指在运行时确定程序应该调用哪个具体的方法或函数的过程。分派的方式可以分为静态分派和动态分派两种,静态分派在编译器,如方法重装,动态分派在运行期,如方法重写。

参考文档:
https://refactoringguru.cn/design-patterns/visitor

文章来源:https://blog.csdn.net/zhangyifang_009/article/details/135005659
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。