访问者模式 (Visitor Pattern) 是一种行为型设计模式,它用于将数据结构和在数据结构上的操作分离开来。访问者模式可以让你在不修改数据结构的情况下,定义新的操作。
访问者模式包含以下主要角色:
【例】给宠物喂食:现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。类图如下:
具体实现代码如下:
Person.java
//抽象访问者角色接口
public interface Person {
//给宠物猫喂食
void feed(Cat cat);
//给宠物狗喂食
void feed(Dog dog);
}
Animal.java
//抽象元素角色类
public interface Animal {
//接受访问者访问的功能
void accept(Person person);
}
Cat.java
//具体元素角色类(宠物猫)
public class Cat implements Animal{
@Override
public void accept(Person person) {
//访问者给宠物猫喂食
person.feed(this);
System.out.println("宠物猫接受喂食");
}
}
Dog.java
//具体元素角色类(宠物狗)
public class Dog implements Animal{
@Override
public void accept(Person person) {
//访问者给宠物狗喂食
person.feed(this);
System.out.println("宠物狗接受喂食");
}
}
Owner.java
//具体访问者角色类(宠物主人)
public class Owner implements Person{
@Override
public void feed(Cat cat) {
System.out.println("主人给猫喂食");
}
@Override
public void feed(Dog dog) {
System.out.println("主人给狗喂食");
}
}
SomeOne.java
//具体访问者角色类(其他人)
public class SomeOne implements Person{
@Override
public void feed(Cat cat) {
System.out.println("其他人给猫喂食");
}
@Override
public void feed(Dog dog) {
System.out.println("其他人给猫喂食");
}
}
Home.java()
//对象结构类
public class Home {
//声明一个集合对象,用来存储元素对象
private List<Animal> nodeList = new ArrayList<>();
//添加元素
public void add(Animal animal){
nodeList.add(animal);
}
public void action(Person person){
//遍历集合,获取每一个元素,让访问者访问每一个元素
for (Animal animal : nodeList) {
animal.accept(person);
}
}
}
Client.java
public class Client {
public static void main(String[] args) {
//创建 Home 对象
Home home = new Home();
//添加元素到 Home 对象中
home.add(new Dog());
home.add(new Cat());
//创建主人对象
Owner owner = new Owner();
//让主人喂食所有的宠物
home.action(owner);
System.out.println("===============");
//创建其他人对象
SomeOne someOne = new SomeOne();
//让其他人喂食所有的宠物
home.action(someOne);
}
}
结果如下:
主人给狗喂食
宠物狗接受喂食
主人给猫喂食
宠物猫接受喂食
===============
其他人给猫喂食
宠物狗接受喂食
其他人给猫喂食
宠物猫接受喂食
(1)访问者模式的优点和缺点如下:
(2)综上,访问者模式适合在访问操作的种类比较固定的情况下使用,同时访问者的使用场景也是比较局限的,需要根据具体的场景来判断是否使用。
(1)访问者模式适用于以下场景:
(2)需要注意的是,访问者模式的使用需要权衡代码的复杂性和可维护性,因此在选择使用访问者模式时,需要根据具体的需求和场景来判断是否合适。
事实上,访问者模式用到了一种名为双分派的技术。
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap()
,map 变量的静态类型是 Map,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派 (Dispatch),分派又分为两种,即静态分派和动态分派。
通过方法的重写支持动态分派。
public class Animal {
public void execute() {
System.out.println("Animal");
}
}
public class Dog extends Animal {
@Override public void execute() {
System.out.println("dog");
}
}
public class Cat extends Animal {
@Override public void execute() {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a1 = new Dog();
a1.execute();
Animal a2 = new Cat();
a2.execute();
}
}
运行结果如下:
dog
cat
上面代码的结果大家应该很容易想到,这不就是多态吗!运行执行的是子类中的方法。Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
通过方法重载支持静态分派。
public class Animal {
}
public class Dog extends Animal {
}
public class Cat extends Animal {
}
public class Execute {
public void execute(Animal a) {
System.out.println("Animal");
}
public void execute(Dog d) {
System.out.println("dog");
}
public void execute(Cat c) {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal a1 = new Dog();
Animal a2 = new Cat();
Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}
运行结果如下:
animal
animal
animal
这个结果可能出乎一些人的意料了,为什么呢?因为重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者 (receiver) 的运行时区别,还要根据参数的运行时区别。
(1)双分派技术 (Double Dispatch) 是一种多态性的应用,它允许在运行时根据两个对象的类型来确定方法的调用:
(2)具体来说,双分派技术通过多次派发来确定要执行的方法。首先,根据消息接收者的类型,选择适当的方法版本。然后,根据方法的参数类型,再次选择适当的方法版本。这种双重派发的方式使得程序能够灵活地根据多个对象的类型进行方法调用,从而实现更加动态和灵活的行为。
一个常见的应用场景是访问者模式,其中访问者对象根据元素对象和自身的类型来决定要执行的操作方法。通过双分派技术,可以在访问者模式中根据元素和访问者的具体类型来选择正确的访问方法。
(3)下面以图形绘制为例,展示双分派技术的应用过程:假设有一个图形类库,其中定义了图形类 Shape
和绘制器类 Drawer
,其中 Shape 类有几个子类 Circle、Rect 和 Triangle,Drawer 类有几个子类 ColorDrawer、GrayDrawer 和 RedDrawer。现在需要根据不同的图形和绘制器来绘制不同的图形。
使用传统的单分派多态性方式来实现,需要为每个图形类和绘制器类的组合定义对应的 draw 方法,这个方法的实现是以所有可能的组合为基础,实现的类将有很多重复的代码。而双分派技术使用双重派发来避免这些重复的代码,具体流程如下:
accept
方法,传入一个 Drawer 实例作为参数,其中 accept 方法依赖于具体的 Shape
子类。public abstract class Shape {
public abstract void accept(Drawer drawer);
}
public class Circle extends Shape {
@Override
public void accept(Drawer drawer) {
drawer.drawCircle(this);
}
}
public class Rect extends Shape {
@Override
public void accept(Drawer drawer) {
drawer.drawRect(this);
}
}
public class Triangle extends Shape {
@Override
public void accept(Drawer drawer) {
drawer.drawTriangle(this);
}
}
Drawer
子类。public abstract class Drawer {
public abstract void drawCircle(Circle circle);
public abstract void drawRect(Rect rect);
public abstract void drawTriangle(Triangle triangle);
}
public class ColorDrawer extends Drawer {
@Override
public void drawCircle(Circle circle) {
System.out.println("绘制一个彩色的圆形");
}
@Override
public void drawRect(Rect rect) {
System.out.println("绘制一个彩色的矩形");
}
@Override
public void drawTriangle(Triangle triangle) {
System.out.println("绘制一个彩色的三角形");
}
}
public class GrayDrawer extends Drawer {
@Override
public void drawCircle(Circle circle) {
System.out.println("绘制一个灰色的圆形");
}
@Override
public void drawRect(Rect rect) {
System.out.println("绘制一个灰色的矩形");
}
@Override
public void drawTriangle(Triangle triangle) {
System.out.println("绘制一个灰色的三角形");
}
}
public class RedDrawer extends Drawer {
@Override
public void drawCircle(Circle circle) {
System.out.println("绘制一个红色的圆形");
}
@Override
public void drawRect(Rect rect) {
System.out.println("绘制一个红色的矩形");
}
@Override
public void drawTriangle(Triangle triangle) {
System.out.println("绘制一个红色的三角形");
}
}
public class Main {
public static void main(String[] args) {
Shape shape = new Circle();
Drawer drawer = new ColorDrawer();
shape.accept(drawer); //输出结果为: 绘制一个彩色的圆形
}
}
在上述示例中,accept 方法根据具体的 Shape 子类,调用对应的 Drawer 子类中的方法,在 Drawer 类中,具体的 drawCircle、drawRect 和 drawTriangle 方法会根据具体的 Shape 子类调用正确的方法。这样,通过双重派发技术,可以在运行时根据 Shape 子类和 Drawer 子类的具体类型来确定要调用的方法,从而避免了大量的重复代码。
(4)说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。