一、理论
Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
主要将数据结构与数据操作分离,解决 数据结构和操作耦合性问题
访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
访问者模式主要应用场景是:需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联),同时需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决
- Visitor(访问者)
抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。 - ConcreteVisitor(具体访问者)
它影响访问者访问到一个类后该怎么干,要做什么事情。 - Element(抽象元素)
接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。 - ConcreteElement(具体元素)
实现accept方法,通常是visitor.visit(this)
-ObjectStruture(结构对象)
元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,在项目中,一般很少抽象出这个角色。
双分派
一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参量统称做方法的宗量。
根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言和多分派语言。单元分派语言根据一个宗量的类型(真实类型)进行对方法的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。
C++和Java以及Smaltalk都是单分派语言;多分派语言的例子包括CLOS和Cecil。按照这样的区分,C++和Java就是动态的单分派语言,因为这两种语言的动态分派仅仅会考虑到方法的接收者的类型,同时又是静态的多分派语言,因为这两种语言对重载方法的分派会考虑到方法的接收者的类型和方法所有参量的类型。
在一个支持动态单分派的语言里面,有两个条件决定了一个请求会调用哪一个操作:一是请求的名字,二是接收者的真实类型。单分派限制了方法的选择过程,使得只有一个宗量可以被考虑到,这个宗量通常就是方法的接收者。在JAVA语言里面,如果一个操作是作用于某个类型不明的对象上面的。那么这个对象的真实类型测试仅会发生一次。这个就是动态的单分派的特征。
一言以蔽之,JAVA语言支持静态的多分派和动态的单分派。
一个方法根据两个宗量的类型来决定执行不同的代码,这就是“双分派”或者“多重分派”。Java不支持动态的多分派。但可以通过使用设计模式,在Java语言里面实现动态的双重分派(ps:就是“伪双重分派”是由两次的单分派组成)。
二、代码
企业内有2类员工,工程师和项目经理,每个人都有姓名、工资;考察工作量时:工程师计算工作量(代码数量),项目经理计算项目数量。CTO关注工作量,CFO关注工资。
访问者
public interface IVisitor {
void visit(EngineerStaff engineerStaff);
void visit(ManagerStaff managerStaff);
}
ConcreteVisitor(具体访问者):
public class CTOVisitor implements IVisitor {
@Override
public void visit(EngineerStaff engineerStaff) {
System.out.println("工程师:" + engineerStaff.getName() + "的工作量是:" + engineerStaff.getWorks());
}
@Override
public void visit(ManagerStaff managerStaff) {
System.out.println("项目经理:" + managerStaff.getName() + "的工程量是:" + managerStaff.getProject());
}
}
public class CFOVisitor implements IVisitor {
@Override
public void visit(EngineerStaff engineerStaff) {
System.out.println("工程师:" + engineerStaff.getName() + "的工资是:" + engineerStaff.getSalary());
}
@Override
public void visit(ManagerStaff managerStaff) {
System.out.println("项目经理:" + managerStaff.getName() + "的工资是:" + managerStaff.getSalary());
}
}
Element(抽象元素):
public abstract class Staff {
private String name;
private Integer salary;
public String getName() {
return name;
}
public Integer getSalary() {
return salary;
}
public Staff(String name) {
this.name = name;
this.salary = new Random().nextInt(10000);
}
public abstract void accept(IVisitor visitor);
}
ConcreteElement(具体元素):
public class EngineerStaff extends Staff {
public EngineerStaff(String name) {
super(name);
}
public int getWorks() {
return new Random().nextInt(1000);
}
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
public class ManagerStaff extends Staff {
public ManagerStaff(String name) {
super(name);
}
public int getProject() {
return new Random().nextInt(10);
}
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
ObjectStruture(结构对象):
public class Report {
private List<Staff> staffList = new ArrayList<>();
public void report() {
staffList.add(new EngineerStaff("工程师1"));
staffList.add(new EngineerStaff("工程师2"));
staffList.add(new EngineerStaff("工程师3"));
staffList.add(new ManagerStaff("项目经理1"));
staffList.add(new ManagerStaff("项目经理2"));
staffList.add(new ManagerStaff("项目经理3"));
}
public void show(IVisitor visitor) {
for (Staff staff : staffList) {
staff.accept(visitor);
}
}
}
调用者:
public class Client {
public static void main(String[] args) {
Report report = new Report();
report.report();
IVisitor CTO = new CTOVisitor();
IVisitor CFO = new CFOVisitor();
System.out.println("***CTO看报表***");
report.show(CTO);
System.out.println("***CFO看报表***");
report.show(CFO);
}
}
三、总结
- 优点
- 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
- 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统
- 缺点
- 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的, 这样造成了具体元素变更比较困难
- 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素
- 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的.