文章目录
一、什么是访问者模式
访问者模式(Visitor Pattern)是一种将数据结构与数据操作分离的设计模式。是指封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。属于行为型模式。
访问者模式被称为最复杂的设计模式,并且使用频率不高
,设计模式的作者也评价为:大多数情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。
访问者模式的基本思想是,针对系统中拥有固定类型数量的对象结构(元素),在其内提供一个accept()方法来接收访问者对象的访问。不同的访问者对同一元素的访问内容不同,使得相同的元素集合可以产生不同的数据结构。accept()方法可以接收不同的访问者对象,然后在内部将自己(元素)转发到接收到的访问者对象的visit()方法内
。访问者内部对应类型的visit()方法就会得到回调执行,对元素进行操作。也就是通过两次动态分发
(第一次是对访问者的分发accept()方法,第二次是对元素的分发visit()方法),才最终将一个具体的元素传递到一个具体的访问者,如此一来,就解耦了数据结构与操作,且数据操作不会改变元素状态。
访问者模式的核心是,解耦数据结构与数据操作,使得对元素的操作具备优秀的扩展性,可以通过扩展不同的数据操作类型(访问者)实现对相同元素集的不同操作。
1、访问者模式的应用场景
访问者模式在生活场景中也是用的非常多,例如每年年底的KPI考核,KPI考核标准是相对稳定的,但是参与KPI考核的员工可能每年都会发生变化,那么员工就是访问者。我们平时去食堂或者餐厅吃饭,餐厅的菜单和就餐方式是相对稳定的,但是去餐厅就餐的人员是每天都在发生变化的,因此就餐人员就是访问者。
当系统 中存在类型数目稳定(固定)的一类数据结构时,可以通过访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会对数据产生任何副作用(脏数据)。
简言之,就是对集合中的不同类型数据(类型数量稳定)进行多种操作,则使用访问者模式,访问者模式适用以下场景:
- 数据结构稳定、作用于数据结构的操作经常变化的场景;
- 需要数据结构与数据操作分离的场景;
- 需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。
2、访问者模式的五大角色
- 抽象访问者(Visitor):接口或抽象类,该类给每一个具体元素(Element)的访问冠以一个行为visit()方法,其参数就是具体的元素(Element)对象。理论上来说,Visitor的方法个数与元素(Element)个数是相等的。如果元素(Element)个数经常变动,会导致Visitor的方法也要进行变动,此时,该情形并不适用访问者模式;
- 具体访问者(ConcreteVisitor):实现对具体元素的操作;
- 抽象元素(Element):接口或抽象类,定义了一个接受访问者访问的方法accept(),表示所有元素类型都支持被访问者访问;
- 具体元素(Concrete Element):具体元素类型,提供接受访问者的具体实现。通常的实现都为:visitor.visit(this);
- 结构对象(ObjectStructure):该类内部维护了元素集合,并提供方法接受访问者对该集合所有元素进行操作。
3、访问者模式的优缺点
优点:
- 解耦了数据结构与数据操作,使得操作集合可以独立变化;
- 扩展性好:可以通过扩展访问角色,实现对数据集的不同操作;
- 元素具体类型并非单一,访问者均可操作;
- 各角色职责分离,符合单一职责原则。
缺点:
- 无法增加元素类型:若系统数据结构对象易于变化,经常有新的数据对象增加进来,则访问者类必须增加对应元素类型的操作,违背了开闭原则;
- 具体元素变更困难:具体元素增加属性、删除属性等操作会导致对应的访问者类需要进行相应的修改,尤其当有大量访问者类时,修改范围太大;
- 违背依赖倒置原则:为了达到“区别对待”,访问者依赖的是具体元素类型,而不是抽象。
二、实例
1、访问者模式的通用写法
// 抽象元素
public interface IElement {
void accept(IVisitor visitor);
}
// 具体元素
public class ConcreteElementA implements IElement {
public void accept(IVisitor visitor) {
visitor.visit(this);
}
public String operationA() {
return this.getClass().getSimpleName();
}
}
// 具体元素
public class ConcreteElementB implements IElement {
public void accept(IVisitor visitor) {
visitor.visit(this);
}
public int operationB() {
return new Random().nextInt(100);
}
}
// 抽象访问者
public interface IVisitor {
void visit(ConcreteElementA element);
void visit(ConcreteElementB element);
}
// 具体访问者
public class ConcreteVisitorA implements IVisitor {
public void visit(ConcreteElementA element) {
String result = element.operationA();
System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
}
public void visit(ConcreteElementB element) {
int result = element.operationB();
System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
}
}
// 具体访问者
public class ConcreteVisitorB implements IVisitor {
public void visit(ConcreteElementA element) {
String result = element.operationA();
System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
}
public void visit(ConcreteElementB element) {
int result = element.operationB();
System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
}
}
// 结构对象
public class ObjectStructure {
private List<IElement> list = new ArrayList<IElement>();
{
this.list.add(new ConcreteElementA());
this.list.add(new ConcreteElementB());
}
public void accept(IVisitor visitor) {
for (IElement element : this.list) {
element.accept(visitor);
}
}
}
2、宠物喂食实例
现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。
// 抽象访问者接口
public interface Person {
void feed(Cat cat);
void feed(Dog dog);
}
// 创建不同的具体访问者角色(主人和其他人),都需要实现 Person 接口
public class Owner implements Person {
@Override
public void feed(Cat cat) {
System.out.println("主人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("主人喂食狗");
}
}
public class Someone implements Person {
@Override
public void feed(Cat cat) {
System.out.println("其他人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("其他人喂食狗");
}
}
// 抽象节点 -- 宠物
public interface Animal {
void accept(Person person);
}
// 实现 Animal 接口的 具体节点(元素)
public class Dog implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,汪汪汪!!!");
}
}
public class Cat implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,喵喵喵!!!");
}
}
// 定义对象结构,此案例中就是主人的家
public class Home {
private List<Animal> nodeList = new ArrayList<Animal>();
public void action(Person person) {
for (Animal node : nodeList) {
node.accept(person);
}
}
//添加操作
public void add(Animal animal) {
nodeList.add(animal);
}
}
// 测试类
public class Client {
public static void main(String[] args) {
Home home = new Home();
home.add(new Dog());
home.add(new Cat());
Owner owner = new Owner();
home.action(owner);
Someone someone = new Someone();
home.action(someone);
}
}
3、KPI考核案例
每到年底,管理层就要开始评定员工一年的工作绩效,员工分为工程师和产品经理;管理层有CEO(首席执行官)和CTO(首席技术官)。CTO关注工程师的代码量、产品经理的产品数量;CEO关注KPI。
由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。我们使用访问者模式进行实现。
// 员工抽象类
public abstract class Employee {
public String name;
public int kpi; //员工KPI
public Employee(String name) {
this.name = name;
kpi = new Random().nextInt(10);
}
//接收访问者的访问
public abstract void accept(IVisitor visitor);
}
Employee类定义了员工基本信息及一个accept()方法,该方法表示接受访问者的访问,由具体的子类来实现。访问者是个接口,传入不同的实现类,可以访问不同的数据。
// 工程师
public class Engineer extends Employee {
public Engineer(String name) {
super(name);
}
public void accept(IVisitor visitor) {
visitor.visit(this);
}
//考核指标是每年的代码量
public int getCodeLines(){
return new Random().nextInt(10* 10000);
}
}
// 产品经理
public class Manager extends Employee {
public Manager(String name) {
super(name);
}
public void accept(IVisitor visitor) {
visitor.visit(this);
}
//考核的是每年新产品研发数量
public int getProducts(){
return new Random().nextInt(10);
}
}
工程师考核代码梳理,产品经理考核产品数量,二者的职责不同。也正是因为有这样的差异性,才使得访问者模式能够在这个场景下发挥作用。Employee、Engineer、Manager这三个类型就相当于数据结构,这些类型相对稳定,不会发生变化。
将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的showReport方法查看所有员工的业绩:
public class BusinessReport {
private List<Employee> employees = new LinkedList<Employee>();
public BusinessReport() {
employees.add(new Manager("产品经理A"));
employees.add(new Engineer("程序员A"));
employees.add(new Engineer("程序员B"));
employees.add(new Engineer("程序员C"));
employees.add(new Manager("产品经理B"));
employees.add(new Engineer("程序员D"));
}
public void showReport(IVisitor visitor){
for (Employee employee : employees) {
employee.accept(visitor);
}
}
}
// 访问者接口,通常由重载方法完成,有几种元素就有几个重载方法
public interface IVisitor {
void visit(Engineer engineer);
void visit(Manager manager);
}
// CEO关注KPI
public class CEOVistitor implements IVisitor {
public void visit(Engineer engineer) {
System.out.println("工程师" + engineer.name + ",KIP:" + engineer.kpi);
}
public void visit(Manager manager) {
System.out.println("经理:" + manager.name + ",KPI:" + manager.kpi + ",产品数量:" + manager.getProducts());
}
}
// CTO关注代码行数
public class CTOVistitor implements IVisitor {
public void visit(Engineer engineer) {
System.out.println("工程师" + engineer.name + ",代码行数:" + engineer.getCodeLines());
}
public void visit(Manager manager) {
System.out.println("经理:" + manager.name + ",产品数量:" + manager.getProducts());
}
}
// 客户端代码
public class Test {
public static void main(String[] args) {
BusinessReport report = new BusinessReport();
System.out.println("==========CEO看报表===============");
report.showReport(new CEOVistitor());
System.out.println("==========CTO看报表===============");
report.showReport(new CTOVistitor());
}
}
在上述案例中,Employee扮演了Element角色,Engineer和Manager都是ConcreteElement;CEOVisitor和CTOVisitor都是具体的Visitor对象;BusinessReport就是ObjectStructure。
小总结
访问者模式的最大有点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只需要新实现一个访问者接口的类,从而达到数据与数据操作相分离的效果。如果不使用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用if-else和类型转换,这使得代码难以维护。
我们要根据具体情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定,是否需要经常定义新的操作,使用访问者模式是否能优化我们的代码,而不是使我们的代码变得更复杂。
三、分派
1、什么是分派
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap() ,map变量的静态类型是 Map,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派
。
2、静态分派
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
我们来看这个例子:
public class Main {
public void test(String string){
System.out.println("string" + string);
}
public void test(Integer integer){
System.out.println("integer" + integer);
}
public static void main(String[] args) {
String string = "1";
Integer integer = 1;
Main main = new Main();
main.test(integer);// integer1
main.test(string); // string1
}
}
调用test方法时,编译器根据参数类型和个数,判断方法的版本。
再来看这个例子:
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); // Animal
exe.execute(a1); // Animal
exe.execute(a2); // Animal
}
}
这个结果可能会出乎很多人意料:重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。判断参数类型和参数个数,找到对应方法,这就是多分派的 概念,因为我们有一个以上的考量标准。所以Java是静态多分派的语言,也就是重载
。
3、动态分派
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。最典型的应用就是多态。
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 a = new Dog();
a.execute();
Animal a1 = new Cat();
a1.execute();
}
}
上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。
Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
动态分派判断的方法是在运行时获取到对象的实际引用类型,再确定方法的版本,而由于此时判断的依据只是实际引用类型,只有一个判断一句,所以这就是单分派的概念,此时我们的考量标准只有一个,即变量的实际引用类型。相应的,这说明Java是动态单分派的语言
。
4、双分派
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。
public class Animal {
public void accept(Execute exe) {
exe.execute(this); // 第二次分派
}
}
public class Dog extends Animal {
public void accept(Execute exe) {
exe.execute(this); // 第二次分派
}
}
public class Cat extends Animal {
public void accept(Execute exe) {
exe.execute(this); // 第二次分派
}
}
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 d = new Dog();
Animal c = new Cat();
Execute exe = new Execute();
a.accept(exe); // 第一次分派
d.accept(exe); // 第一次分派
c.accept(exe); // 第一次分派
}
}
在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也 将自己this作为参数传递进去,这里就完成了第二次分派 ,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了
。
5、访问者模式中的伪动态双分派
动态双分派指的是在运行时根据实际对象的类型决定需要调用的方法,并且方法的调用不仅仅取决于参数的类型,还取决于接收者的类型。这种多态的实现方式可以更准确地确定方法执行的上下文,从而提高代码的可读性和灵活性。在使用动态双分派时,通常需要使用反射、接口或者抽象类等机制来实现方法的动态调用。
通过前面的分析,我们知道了Java是静态多分派、动态单分派的语言。Java底层不支持动态的双分派。但是通过使用设计模式,也可以在Java语言里实现伪动态双分派。在访问者模式中使用的就是伪动态双分派,而访问者模式实现的手段就是进行了两次动态单分派来达到这个效果。
在上面的KPI考核案例
中,BusinessReport 的showReport方法:
public void showReport(IVisitor visitor){
for (Employee employee : employees) {
employee.accept(visitor);
}
}
这里就是依据Employee和IVisitor两个实际类型决定了showReport方法的执行结果,从而决定了accept方法的动作。
分析accept方法的调用过程:
1、当调用accept方法时,根据Employee的实际类型决定是调用Engineer还是Manager的accept方法。
2、这时accept方法的版本已经确定,假如是Engineer,它的accept方法是调用下面这行代码:
public void accept(IVisitor visitor) {
visitor.visit(this);
}
此时的this就是Engineer类型,所以对应的是IVisitor接口的visit(Engineer engineer)方法,此时需要再根据访问者的实际类型确定visit()方法的版本,如此一来,就完成了动态双分派的过程。
以上的过程就是通过两次单分派,达到了根据两个实际类型确定一个方法的行为的效果。第一次对accept()方法进行动态分派,第二次对访问者的visit()方法进行动态分派。
注!这里的this的类型并不是动态确定的,你写在哪个类当中,它的静态类型就是哪个类,这是在编译期就确定的,不确定的是它的实际类型,这里一定要搞清楚。
注!动态双分派一定要是两次动态分派合起来才叫动态双分派,动态分派是在运行时发生的,与静态分派有着本质区别,不可以说一次动态分派加一次静态分派就是动态双分派。
四、访问者模式在源码中的应用
1、FileVisitor
我们来看JDK的NIO模块下的FIleVisitor接口,它提供了递归遍历文件树的支持。这个接口上的方法表示了遍历过程中的关键过程,允许你在文件被访问、目录将被访问、目录已被访问、发生错误等等过程上进行控制;换句话说,这个接口在文件被访问前、访问中和访问后,以及产生错误的时候都有相应的钩子程序进行处理。
public interface FileVisitor<T> {
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException;
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException;
FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException;
FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException;
}
调用FileVisitor中的方法,会返回访问结果FileVisitResult对象值,用于决定当前操作完成后接下来 该如何处理。FileVisitResult的标准返回值存放到FileVisitResult枚举类型中:
- FileVisitResult.CONTINUE:这个访问结果表示当前的遍历过程将会继续;
- FileVisitResult.SKIP_SIBLINGS:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点;
- FileVisitResult.SKIP_SUBTREE:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前目录下的所有节点;
- FileVisitResult.TERMINATE:这个访问结果表示当前的遍历过程将会停止。
通过它去遍历文件树会比较方便,比如查找文件夹内符合某个条件的文件或者某一天内所创建的文件,这个类中都提供了相对应的方法。我们来看一下它的实现其实也非常简单:
public class SimpleFileVisitor<T> implements FileVisitor<T> {
protected SimpleFileVisitor() {
}
@Override
public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(dir);
Objects.requireNonNull(attrs);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(file);
Objects.requireNonNull(attrs);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException
{
Objects.requireNonNull(file);
throw exc;
}
@Override
public FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException
{
Objects.requireNonNull(dir);
if (exc != null)
throw exc;
return FileVisitResult.CONTINUE;
}
}
我们来看其基本使用:
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
public class FileVisitorTest {
public static void main(String[] args) {
try {
// 使用FileVisitor对目录进行遍历
Files.walkFileTree(Paths.get("F:", "\\gvisitor"), new SimpleFileVisitor<Path>() {
// 在访问子目录前触发该方法
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("正在访问" + dir + "目录");
return FileVisitResult.CONTINUE;
}
// 在访问文件时触发该方法
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("正在访问" + file + "文件");
if (file.endsWith("FileVisitorTest.java")) {
System.out.println("------FileVisitorTest.java,文件内容-----");
List<String> list = Files.readAllLines(file);
// 打印出文件的内容
System.out.println(list);
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
// 在访问失败时触发该方法
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
// 写一些具体的业务逻辑
return super.visitFileFailed(file, exc);
}
// 在访问目录之后触发该方法
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
// 写一些具体的业务逻辑
return super.postVisitDirectory(dir, exc);
}
});
}catch (Exception e){
e.printStackTrace();
}
}
}
2、BeanDefinitionVisitor
Spring IOC中有个BeanDefinitionVisitor类,其中有一个visitBeanDefinition方法,我们看一下源码:
public void visitBeanDefinition(BeanDefinition beanDefinition) {
visitParentName(beanDefinition);
visitBeanClassName(beanDefinition);
visitFactoryBeanName(beanDefinition);
visitFactoryMethodName(beanDefinition);
visitScope(beanDefinition);
if (beanDefinition.hasPropertyValues()) {
visitPropertyValues(beanDefinition.getPropertyValues());
}
if (beanDefinition.hasConstructorArgumentValues()) {
ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
visitIndexedArgumentValues(cas.getIndexedArgumentValues());
visitGenericArgumentValues(cas.getGenericArgumentValues());
}
}
我们看到该方法中,分别访问了其他数据,比如父类的名字、自己的类名、在IOC容器中的名称等各种信息。