访问者模式是一种将数据操作和数据结构分离的设计模式,针对一种比较稳定的数据结构,通过接收不同的访问者,来实现对数据结构的访问遍历等操作,一般稳定的对象结构会提供一个accept
方法来接收访问者,而访问对象会提供一个visit
方法来对稳定数据结构对象进行访问,这与一次真实的访问事件是类似的。
定义
封装一些作用于某稳定数据结构各元素的操作,实现在不改变这个数据结构的前提下来对数据提供一些新的操作
场景
- 对象结构比较稳定,经常需要定义新的操作
- 需要对一种稳定的数据结构进行很多不同的操作,而且不能污染该数据结构,也不希望增加新操作时修改这种稳定的数据结构
角色
抽象访问者
接口或者抽象类,通过visit定义对每一种元素的访问行为
具体访问者
抽象访问者的实现类,定义了每次访问的具体实现
抽象元素
接口或者抽象类,通过accept方法接受一个访问者对象
具体元素
数据结构中被定义的各种具体元素对象,都实现于抽象元素
元素集合(稳定数据结构)
稳定数据结构对象,由一系列抽象元素类型组成
代码说明
在公司中,每个人的绩效成绩大多都是由kpi来决定的,那么在某一次绩效评定中,每个员工都涉及两项数据:姓名和kpi,这就构成了一种稳定的数据结构。我们定义为一个接口。
/**
* 稳定的数据结构抽象类
*/
public abstract class AbstractStaff {
protected String name;
protected float kpi;
/**
* 接收访问者的方法
* @param visitor 访问者对象
*/
public abstract void accept(IVisitor visitor);
}
然后在公司里面,会有研发、销售、管理岗等等,他们的数据评级有可能不一样。这里,我们创建两个具体的数据结构对象:研发和销售。
/**
* 研发人员具体类,规定具体的kpi需要乘以系数2
*/
public class RdStaff extends AbstractStaff {
public RdStaff(String name, float kpi) {
super(name, kpi);
this.kpi *= 2;
}
/**
* @param visitor 访问者对象
*/
@Override public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
/**
* 销售人员具体类,规定具体的kpi需要乘以1.5
*/
public class SailStaff extends AbstractStaff {
public SailStaff(String name, float kpi) {
super(name, kpi);
this.kpi *= 1.5f;
}
/**
* @param visitor 访问者对象
*/
@Override public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
然后我们需要创建抽象访问者和具体访问者
/**
* 抽象访问者
*/
public interface IVisitor {
/**
* 访问方法
*
* @param staff 抽象元素类型
*/
public void visit(AbstractStaff staff);
}
具体访问者,我们可以根据元素类型来创建不同的访问者,也可以创建同一个访问者来接收不同的访问类型。为了减少篇幅,我们采用第二种方式。
/**
* 具体访问者
*/
public class Visitor implements IVisitor {
/**
* 此处只提供了一个访问方法,可以根据不同的对象类型来进行不同的操作
* 这样做会引入很多分支判断
* 实际上可以创建多个访问方法或者新建多个访问者类来解决这个问题
*
* @param staff 抽象元素类型
*/
@Override public void visit(AbstractStaff staff) {
if (staff instanceof RdStaff) {
System.out.println("研发人员:" + staff.name + "-" + staff.kpi);
} else if (staff instanceof SailStaff) {
System.out.println("销售人员:" + staff.name + "-" + staff.kpi);
} else {
System.out.println("其他人员:" + staff.name + "-" + staff.kpi);
}
}
}
这样,我们只需要修改IVisitor
实现类就可以来对任何一种类型的员工进行信息采集。实际上以上代码中没有体现出稳定数据结构的特征,仅仅体现了元素的特征。但是,我们只需要认为一个公司是由很多员工组成的,也就是说员工的集合就是一种稳定的数据结构。
关于源码
研究过字节码的同学肯定知道,java字节码文件是一种格式非常严谨的紧凑型文件,它的格式也可以被解析成一种稳定的数据结构对象。在著名的字节码注入项目ASM中,对字节码文件的访问和修改都是通过访问者模式来进行的。她里面涉及了几个类定义,分别是ClassReader
、ClassVisitor
、MethodVisitor
、FieldVisitor
等,其中ClassReader
是对字节码文件进行解析生成稳定的数据结构,它内部有一个accept
方法:
/**
* Makes the given visitor visit the Java class of this {@link ClassReader}
* . This class is the one specified in the constructor (see
* {@link #ClassReader(byte[]) ClassReader}).
*
* @param classVisitor
* the visitor that must visit this class.
*/
public void accept(final ClassVisitor classVisitor, final int flags) {
accept(classVisitor, new Attribute[0], flags);
}
然后看一下ClassVisitor
,它内部提供了一系列的访问方法,用来访问不同的元素:
/**
* A visitor to visit a Java class.
*/
public abstract class ClassVisitor {
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
}
public void visitSource(String source, String debug) {
}
public void visitOuterClass(String owner, String name, String desc) {
}
public void visitAttribute(Attribute attr) {
}
public void visitInnerClass(String name, String outerName,
String innerName, int access) {
}
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
}
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
}
}
由于篇幅原因,这里省略部分方法。有兴趣的同学可以研究下,该项目用来处理java字节码的效率相对其他的项目要高一些。
总结
在一般情况下,是不需要使用访问者模式的,但是,如果你确定了可以使用访问者模式的条件后,那么就可以放心使用它了。访问者模式使各个角色的职责分离,符合单一职责原则;使数据结构和操作分离,使操作可以自我变化;也具有很强的扩展性和灵活性。当然,他也有自己的不足,具体元素的修改势必会导致涉及的代码进行很大的改动;元素的访问和类型判断都依赖具体类,违反了依赖倒置和迪米特原则。