访问者模式(Visitor Pattern),封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
首先我们要明确一点就是访问者模式适用于数据结构相对稳定的系统。它是将数据的操作与数据结构进行分离了,如果某个系统的数据结构相对稳定,但是操作算法易于变化的话,就比较适用适用访问者模式,因为访问者模式使得算法操作的增加变得比较简单了。
访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
模式结构下图是访问者模式的UML结构图:
访问者模式主要包含如下几个角色:
(1)Visitor:接口或者抽象类,它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法数理论上来讲与元素个数是一样的,因此,访问者模式要求元素的类族要稳定,如果经常添加、移除元素类,必然会导致频繁地修改Visitor接口,如果这样则不适合使用访问者模式。
(2)ConcreteVisitor1、ConcreteVisitor2:具体的访问类,它需要给出对每一个元素类访问时所产生的具体行为。
(3)Element:元素接口或者抽象类,它定义了一个接受访问者的方法(Accept),其意义是指每一个元素都要可以被访问者访问。
(4)ConcreteElementA、ConcreteElementB:具体的元素类,它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
(5)ObjectStructure:定义当中所说的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素供访问者访问。
在访问者模式中对象结构存储了不同类型的对象,以便不同的访问者来访问。从上面的UML结构图中我们可以看出,访问者模式主要分为两个层次结构,一个是访问者层次结构,提供了抽象访问者和具体访问者,主要用于什么一些操作。一个是元素层次结构,提供了抽象元素和具体元素,主要用于声明Accept操作。
在访问者模式中相同的访问者可以以不同的方式访问不同的元素,所以在访问者模式中增加新的访问者无需修改现有代码,可扩展行强。
同时在访问者模式用到了一种双分派的技术,所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别(Run time type),还要根据参数的运行时区别。在访问者模式中,客户端将具体状态当做参数传递给具体访问者,这里完成第一次分派,然后具体访问者作为参数的“具体状态”中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派。双分派意味着得到的执行操作决定于请求的种类和接受者的类型。
场景:很多人都有养宠物的习惯,这里就以此为例
访问者角色:可以给宠物喂食的人
具体访问者角色:主人、其他人
抽象元素角色:动物抽象类
具体元素角色:宠物狗、宠物猫
package com.dongguo.visitor;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:55
* @description: 抽象访问者接口 -- 人
*/
public abstract class Person {
/**
* 喂食狗
*/
public abstract void feed(Cat cat);
/**
* 喂食猫
*/
public abstract void feed(Dog dog);
}
package com.dongguo.visitor;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:56
* @description: 具体访问者角色 -- 主人
*/
public class Owner extends Person {
@Override
public void feed(Cat cat) {
System.out.println("主人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("主人喂食狗");
}
}
package com.dongguo.visitor;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:57
* @description: 具体访问者角色 -- 其他人
*/
public class Someone extends Person {
@Override
public void feed(Cat cat) {
System.out.println("其他人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("其他人喂食狗");
}
}
package com.dongguo.visitor;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:57
* @description: 抽象节点(元素)角色 -- 宠物
*/
public abstract class Animal {
//吃食操作
public abstract void accept(Person person);
}
package com.dongguo.visitor;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:58
* @description: 具体节点(元素)角色 -- 宠物狗
*/
public class Dog extends Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("狗粮好好吃,汪汪汪!!!");
}
}
package com.dongguo.visitor;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:58
* @description: 具体节点(元素)角色 -- 宠物猫
*/
public class Cat extends Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("猫粮好好吃,喵喵喵!!!");
}
}
package com.dongguo.visitor;
import java.util.ArrayList;
import java.util.List;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:59
* @description: 结构对象角色类 -- 主人家
*/
public class Home {
private List nodeList = new ArrayList();
public void action(Person person) {
for (Animal node : nodeList) {
node.accept(person);
}
}
/**
* 添加操作
* @param animal
*/
public void add(Animal animal) {
nodeList.add(animal);
}
}
package com.dongguo.visitor;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:59
* @description:
*/
public class Client {
public static void main(String[] args) {
Home owerHome = new Home();
owerHome.add(new Dog());
owerHome.add(new Cat());
Owner owner = new Owner();
owerHome.action(owner);
Someone someone = new Someone();
owerHome.action(someone);
}
}
运行结果:
主人喂食狗
狗粮好好吃,汪汪汪!!!
主人喂食猫
猫粮好好吃,喵喵喵!!!
其他人喂食狗
狗粮好好吃,汪汪汪!!!
其他人喂食猫
猫粮好好吃,喵喵喵!!!
优点
1、使得新增新的访问操作变得更加简单。
2、能够使得用户在不修改现有类的层次结构下,定义该类层次结构的操作。
3、将有关元素对象的访问行为集中到一个访问者对象中,而不是分散搞一个个的元素类中。
缺点1、增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求。 2、破坏封装。当采用访问者模式的时候,就会打破组合类的封装。
3、比较难理解。貌似是最难的设计模式了。
模式适用场景1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
模式总结1、访问者模式封装了对象结构元素之上的操作,使得新增元素的操作变得非常简单。所以它比较适用于那么对象结构很少变化的类。
2、访问者模式中对象结构存储了不同类型的元素对象,以供不同访问者访问。