访问者模式定义
定义:封装一些作用于某种数据中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
通用类图如下:
思考
为什么需要访问者模式呢,这就要放到OOP之中了,在面向对象编程的思想中,我们使用类来组织属性,以及对属性的操作,那么我们理所当然的将访问操作放到了类的内部,这样看起来没问题,但是当我们想要使用另一种遍历方式要怎么办呢,我们必须将这个类进行修改,这在设计模式中是大忌,在设计模式中就要保证,对扩展开放,对修改关闭的开闭原则。
因此,我们思考,可不可以将访问操作独立出来变成一个新的类,当我们需要增加访问操作的时候,直接增加新的类,原来的代码不需要任何的改变,如果可以这样做,那么我们的程序就是好的程序,因为可以扩展,符合开闭原则。而访问者模式就是实现这个的,使得使用不同的访问方式都可以对某些元素进行访问。
角色定义
- Visitor——抽象访问者
抽象类或者接口,声明访问者可以访问哪些数据,具体到程序中就是visit()
方法的参数定义对象是可以被访问。 - ConcreteVisitor——具体访问者
它影响访问者访问到一个类后该怎么干,要做什么事情。 - Element——抽象元素
接口或抽象类,声明接受哪一类访问者访问,程序上是通过accept()
方法中的参数来定义。 - ConcreteElement——具体元素
实现accept()
方法,通常是visitor.visit(this)
,基本上就形成了一种模式了。 - ObjectStrutrue——结构对象
元素产生者,一般容纳在多个不同类、不同接口的容器,如List,Set,Map等,在项目中,一般很少抽象处这个角色。
通用源码
抽象元素
public abstract class Element {
/**
* 定义业务逻辑
*/
abstract void doSomething();
/**
* 允许谁来访问
* @param ivisitor
*/
abstract void accept(IVisitor ivisitor);
}
抽象元素有两种方法:一是本身的业务逻辑,也就是元素作为一个业务处理单元必须完成的职责;另外一个是允许哪一个访问者来访问。
具体元素
public class ConcreteElement1 extends Element{
@Override
void doSomething() {
System.out.println("111111111111111");
}
@Override
void accept(IVisitor visitor) {
visitor.visit(this);
}
}
public class ConcreteElement2 extends Element{
@Override
void doSomething() {
System.out.println("2222222222");
}
@Override
void accept(IVisitor visitor) {
visitor.visit(this);
}
}
抽象访问者
public interface IVisitor {
void visit(ConcreteElement1 element);
void visit(ConcreteElement2 element);
}
具体访问者
public class Visitor implements IVisitor{
@Override
public void visit(ConcreteElement1 element) {
//访问的一些逻辑
element.doSomething();
}
@Override
public void visit(ConcreteElement2 element) {
//访问的一些逻辑
element.doSomething();
}
}
结构对象是产生出不同的元素对象,使用工厂方法模拟
结构对象
public class ObjectStructure {
public static Element createElement(){
Random rand = new Random();
if(rand.nextInt(100)>50){
return new ConcreteElement1();
}else{
return new ConcreteElement2();
}
}
}
接入访问者角色后,对所有具体元素的访问就非常简单了
场景类
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
//获取元素对象
Element e = ObjectStructure.createElement();
//接受访问者访问
e.accept(new Visitor());
}
}
}
通过增加访问者,只要是具体元素就非常容易访问,对元素的遍历就更加容易了,甭管它是什么对象,只要它在一个容器中,都可以通过访问者来访问,任务集中化,这就是访问者模式。
访问者模式的应用
访问者模式的优点
- 符合单一职责原则
具体元素角色也是Employee抽象类的两个子类负责数据的加载,而Visitor类则负责报表的展现,两个不同的职责非常明确地分开来,各自演绎变化。 - 优秀的扩展性
由于职责分开,继续增加对数据的操作是非常快捷的,例如,现在要增加一份给老板看的报表,这份报表格式又有所不同,直接在Visitor中增加一个方法,传递数据后进行整理打印。 - 灵活性非常高
访问者模式的缺点
- 具体元素对访问者公布细节
访问者要访问一个类就必然要求这个类公布一些方法和数据,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的。 - 具体元素变更比较困难
具体元素角色的增加、删除、修改都是比较困难的,就上面那个例子,你想想,你要是想增加一个成员变量,如年龄age,Visitor就需要修改,如果Visitor是一个还好办,多个呢?业务逻辑再复杂点呢? - 违背了依赖倒置原则
访问者依赖的是具体元素,而不是抽象元素,这破坏了依赖倒置原则,特别是在面向对象的编程中,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。
访问者模式的使用场景
一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景。
需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作"污染”这些对象的类。
总结一下,在这种地方你一定要考虑使用访问者模式:业务规则要求遍历多个不同的对象。这本身也是访问者模式出发点,迭代器模式只能访问同类或同接口的数据(当然了,如果你使用instanceof,那么能访问所有的数据,这没有争论),而访问者模式是对迭代器模式的扩充,可以遍历不同的对象,然后执行不同的操作,也就是针对访问的对象不同,执行不同的操作。访问者模式还有一个用途,就是充当拦截器(Interceptor)角色。
访问者模式的扩展
访问者模式是经常用到的模式,虽然你不注意,有可能你起的名字也不是什么Visitor,但是它确实是非常容易使用到的。
统计功能
统计一下公司人员的工资总额。类图如下:
抽象访问者
public interface IVisitor {
//首先定义我可以访问普通员工
public void visit(CommonEmployee commonEmployee);
//其次定义,我还可以访问部门经理
public void visit(Manager manager);
//统计所有员工工资总和
public int getTotalSalary();
}
具体访问者
public class Visitor implements IVisitor{
/**
* 部门经理的工资系数是5
*/
private final static int MANAGE_COEFFICIENT = 5;
/**
* 普通员工的工资系数是2
*/
private final static int COMMON_EMPLOYEE_COEFFICIENT = 2;
/**
* 普通员工的工资总和
*/
private int commonTotalSalary= 0;
/**
* 部门经理的工资总和
*/
private int manageTotalSalary = 0;
@Override
public void visit(CommonEmployee employee) {
System.out.println(this.getCommonEmployee(employee));
calCommonSalary(employee.getSalary());
}
@Override
public void visit(Manager manager) {
System.out.println(this.getManagerInfo(manager));
calManageSalary(manager.getSalary());
}
@Override
public int getTotalSalary() {
return this.manageTotalSalary + this.commonTotalSalary;
}
/**
* 计算经理的工资总和
* @param salary
*/
private void calManageSalary(int salary){
this.manageTotalSalary += salary * MANAGE_COEFFICIENT;
}
private void calCommonSalary(int salary){
this.commonTotalSalary += salary * COMMON_EMPLOYEE_COEFFICIENT;
}
/**
* 组装出基本信息
* @param employee
* @return
*/
private String getBasicInfo(Employee employee) {
String info = "姓名:" + employee.getName() + "\t";
info += "性别:" + (employee.getSex() == Employee.FEMALE ? "女" : "男") + "\t";
info += "薪水:" + employee.getSalary() + "\t";
return info;
}
/**
* 组装出部门经理的信息
* @param manager
* @return
*/
private String getManagerInfo(Manager manager) {
String basicInfo = this.getBasicInfo(manager);
String otherInfo = "业绩:" + manager.getPerformance() + "\t";
return basicInfo + otherInfo;
}
/**
* 组装出普通员工信息
* @param employee
* @return
*/
private String getCommonEmployee(CommonEmployee employee) {
String basicInfo = this.getBasicInfo(employee);
String otherInfo = "工作:" + employee.getJob() + "\t";
return basicInfo + otherInfo;
}
}
场景类
public class Client {
public static void main(String[] args) {
IVisitor visitor = new Visitor();
for (Employee employee : mockEmployee()) {
employee.accept(visitor);
}
System.out.println("本公司的月工资总额是:"+visitor.getTotalSalary());
}
/**
* 模拟出公司的人员情况
* @return
*/
public static List<Employee> mockEmployee(){
List<Employee> employees = new ArrayList<>();
CommonEmployee zhangsan = new CommonEmployee();
zhangsan.setJob("编写程序");
zhangsan.setName("张三");
zhangsan.setSalary(1800);
zhangsan.setSex(designpattern.visitor.Employee.MALE);
employees.add(zhangsan);
CommonEmployee lisi = new CommonEmployee();
lisi.setJob("页面美工");
lisi.setName("李四");
lisi.setSalary(1900);
lisi.setSex(designpattern.visitor.Employee.FEMALE);
employees.add(lisi);
Manager wangwu = new Manager();
wangwu.setName("王五");
wangwu.setPerformance("基本上是负值,但是会拍马屁");
wangwu.setSalary(1879);
wangwu.setSex(Employee.MALE);
employees.add(wangwu);
return employees;
}
}
多个访问者
在实际的项目中,一个对象,多个访问者的情况非常多。其实我们上面例子就应该是两个访问者,为什么呢?报表分两种:第一种是展示表,通过数据库查询,把结果展示出来,这个就类似于我们的那个列表;第二种是汇总表,这个是需要通过模型或者公式计算出来的,一般都是批处理结果,这个类似于我们计算工资总额,这两种报表格式是对同一堆数据的两种处理方式。从程序上看,一个类就有个不同的访问者了。修改该后的类如下。
类图看着挺复杂,其实也没什么复杂的,只是多了两个接口和两个实现类,分别负责展示表和汇总表的业务处理,IVisitor接口没有改变。
汇总类接口
public interface ITotalVisitor extends IVisitor{
void getTotalSalary();
}
具体汇总类
public class TotalVisitor implements ITotalVisitor{
/**
* 部门经理的工资系数是5
*/
private final static int MANAGE_COEFFICIENT = 5;
/**
* 普通员工的工资系数是2
*/
private final static int COMMON_EMPLOYEE_COEFFICIENT = 2;
/**
* 普通员工的工资总和
*/
private int commonTotalSalary= 0;
/**
* 部门经理的工资总和
*/
private int manageTotalSalary = 0;
@Override
public void getTotalSalary() {
System.out.println("本月公司总工资是:" + this.manageTotalSalary + this.commonTotalSalary);
}
@Override
public void visit(CommonEmployee employee) {
calCommonSalary(employee.getSalary());
}
@Override
public void visit(Manager manager) {
calManageSalary(manager.getSalary());
}
/**
* 计算经理的工资总和
* @param salary
*/
private void calManageSalary(int salary){
this.manageTotalSalary += salary * MANAGE_COEFFICIENT;
}
private void calCommonSalary(int salary){
this.commonTotalSalary += salary * COMMON_EMPLOYEE_COEFFICIENT;
}
}
抽象展示类
public interface IShowVisitor extends IVisitor {
/**
* 展示报表
*/
void report();
}
具体展示类
public class ShowVisitor implements IShowVisitor {
private String info = "";
@Override
public void report() {
System.out.println(info);
}
@Override
public void visit(CommonEmployee employee) {
this.info += "\n" + this.getCommonEmployee(employee);
}
@Override
public void visit(Manager manager) {
this.info += "\n" + this.getManagerInfo(manager);
}
/**
* 组装出基本信息
* @param employee
* @return
*/
private String getBasicInfo(Employee employee) {
String info = "姓名:" + employee.getName() + "\t";
info += "性别:" + (employee.getSex() == Employee.FEMALE ? "女" : "男") + "\t";
info += "薪水:" + employee.getSalary() + "\t";
return info;
}
/**
* 组装出部门经理的信息
* @param manager
* @return
*/
private String getManagerInfo(Manager manager) {
String basicInfo = this.getBasicInfo(manager);
String otherInfo = "业绩:" + manager.getPerformance() + "\t";
return basicInfo + otherInfo;
}
/**
* 组装出普通员工信息
* @param employee
* @return
*/
private String getCommonEmployee(CommonEmployee employee) {
String basicInfo = this.getBasicInfo(employee);
String otherInfo = "工作:" + employee.getJob() + "\t";
return basicInfo + otherInfo;
}
}
场景类
public class Client {
public static void main(String[] args) {
IShowVisitor showVisitor = new ShowVisitor();
ITotalVisitor totalVisitor = new TotalVisitor();
for (Employee employee : mockEmployee()) {
employee.accept(showVisitor);
employee.accept(totalVisitor);
}
showVisitor.report();
totalVisitor.getTotalSalary();
}
/**
* 模拟出公司的人员情况
* @return
*/
public static List<Employee> mockEmployee(){
List<Employee> employees = new ArrayList<>();
CommonEmployee zhangsan = new CommonEmployee();
zhangsan.setJob("编写程序");
zhangsan.setName("张三");
zhangsan.setSalary(1800);
zhangsan.setSex(designpattern.visitor.Employee.MALE);
employees.add(zhangsan);
CommonEmployee lisi = new CommonEmployee();
lisi.setJob("页面美工");
lisi.setName("李四");
lisi.setSalary(1900);
lisi.setSex(designpattern.visitor.Employee.FEMALE);
employees.add(lisi);
Manager wangwu = new Manager();
wangwu.setName("王五");
wangwu.setPerformance("基本上是负值,但是会拍马屁");
wangwu.setSalary(1879);
wangwu.setSex(Employee.MALE);
employees.add(wangwu);
return employees;
}
}
双分派
分派( dispatch)是指运行环境按照对象的实际类型为其绑定对应方法体的过程。
说到访问者模式就不得不提一下双分派(double dispatch)问题,什么是双分派呢?我们先来解释一下什么是单分派(single dispatch)和多分派(multiple dispatch),单分派语言处理一个操作是根据请求者的名称和接收到的参数决定的,在Java中有静态绑定和动态绑定之说,它的实现是依据重载(overload)和覆写(override)实现的。
double dispatch(双分派)在选择一个方法的时候,不仅仅要根据消息接收者(receiver) 的运行时型别(Run time type),还要根据参数的运行时型别(Run time type)。这里的消息接收者其实就是方法的调用者。具体来讲就是,对于消息表达式a.m(b),双分派能够按照a和b的实际类型为其绑定对应方法体。
我们来说一个简单的例子。例如,演员演电影角色,一个演员可以扮演多个角色,我们先定义一个影视中的两个角色:功夫主角和白痴配角,代码如下。
角色接口及实现类
public interface Role {
//演员要扮演的角色
}
public class KungFuRole implements Role {
//武功天下第一的角色
}
public class IdiotRole implements Role {
//一个弱智角色
}
抽象角色
public abstract class AbsActor {
//演员都能够演一个角色
public void act(Role role){
System.out.println("演员可以扮演任何角色");
}
//可以演功夫戏
public void act(KungFuRole role){
System.out.println("演员都可以演功夫角色");
}
}
比较简单,这里使用了java的重载
青年演员和老年演员
public class YoungActor extends AbsActor {
//年轻演员最喜欢演功夫戏
public void act(KungFuRole role){
System.out.println("最喜欢演功夫角色");
}
}
public class OldActor extends AbsActor {
//不演功夫角色
public void act(KungFuRole role){
System.out.println("年龄大了,不能演功夫角色");
}
}
场景类
public class Client {
public static void main(String[] args) {
//定义一个演员
AbsActor actor = new OldActor();
//定义一个角色
Role role = new KungFuRole();
//开始演戏
actor.act(role);
actor.act(new KungFuRole());
/*
运行结果:
演员可以扮演任何角色
年龄大了,不能演功夫角色
*/
}
}
重载在编译器期就决定了要调用哪个方法,它是根据role的表面类型而决定调用act(Role role)
方法,这是静态绑定;而Actor的执行方法act则是由其实际类型决定的,这是动态绑定。
一个演员可以扮演很多角色,我们的系统要适应这种变化,也就是根据演员、角色两个对象类型,完成不同的操作任务,该如何实现呢?很简单,我们让访问者模式上场就可以解决该问题,只要把角色类稍稍修改即可,代码如下。
引入访问者模式
public interface Role {
//演员要扮演的角色
public void accept(AbsActor actor);
}
public class KungFuRole implements Role {
//武功天下第一的角色
public void accept(AbsActor actor){
actor.act(this);
}
}
public class IdiotRole implements Role {
//一个弱智角色,由谁来扮演
public void accept(AbsActor actor){
actor.act(this);
}
}
场景类
public class Client {
public static void main(String[] args) {
//定义一个演员
AbsActor actor = new OldActor();
//定义一个角色
Role role = new KungFuRole();
//开始演戏
role.accept(actor);
/*
运行结果如下:
年龄大了,不能演功夫角色
*/
}
}
看到没?不管演员类和角色类怎么变化,我们都能够找到期望的方法运行,这就是双反派。双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型,它是多分派的一个特例。从这里也可以看到Java是一个支持双分派的单分派语言。