访问者模式是设计模式中行为型模式的一种(其他的还有如创建型、结构型),听说是设计模式中比较难理解的一种,最近项目中用到了该模式,所以今天总结和实践一下。
一、访问者模式要解决的问题:
稳定的数据结构和易变的操作耦合问题
二、动手实现访问者模式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