访问者模式
访问者模式(Visitor Pattern):封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
访问者模式就是根据朋友的信息,执行了自己的一个方法。
访问者模式中的角色
- Visitor抽象访问者:抽象类或接口,声明访问者可以访问的元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。
- ConcreteVisitor具体访问者:访问者访问到一个类后该做的事情。
- Element抽象元素:接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。
- ConcreteElement具体元素:实现accept方法,通常是visitor.visit(this),基本上都形成了一种模式了。
- ObjectStruture结构对象:元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,在项目中,一般很少抽象出这个角色。
抽象元素Element:
public abstract class Element {
//定义业务逻辑
public abstract void doSomething();
//允许谁来访问
public abstract void accept(IVisitor visitor);
}
具体元素ConcreteElement1~2:
public class ConcreteElement1 extends Element{
//完善业务逻辑
public void doSomething(){
//业务处理
}
//允许那个访问者访问
public void accept(IVisitor visitor){
visitor.visit(this);
}
}
public class ConcreteElement2 extends Element{
//完善业务逻辑
public void doSomething(){
//业务处理
}
//允许那个访问者访问
public void accept(IVisitor visitor){
visitor.visit(this);
}
}
抽象访问者IVisitor:
public interface IVisitor {
//可以访问哪些对象
public void visit(ConcreteElement1 el1);
public void visit(ConcreteElement2 el2);
}
具体访问者Visitor:
public class Visitor implements IVisitor {
//访问el1元素
public void visit(ConcreteElement1 el1) {
el1.doSomething();
}
//访问el2元素
public void visit(ConcreteElement2 el2) {
el2.doSomething();
}
}
结构对象ObjectStruture:
public class ObjectStruture {
//对象生成器,这里通过一个工厂方法模式模拟
public static Element createElement(){
Random rand = new Random();
if(rand.nextInt(100) > 50){
return new ConcreteElement1();
}else{
return new ConcreteElement2();
}
}
}
场景类Client:
public class Client {
public static void main(String[] args) {
for(int i=0;i<10;i++){
//获得元素对象
Element el = ObjectStruture.createElement();
//接受访问者访问
el.accept(new Visitor());
}
}
}
访问者模式的优点:
- 符合单一职责原则:具体元素角色也就是Employee抽象类的两个子类负责数据的加载,而Visitor类则负责报表的展现,两个不同的职责非常明确地分离开来,各自演绎变化。
- 优秀的扩展性:由于职责分开,继续增加对数据的操作是非常快捷的。
- 灵活性非常高。
访问者模式的缺点:
- 具体元素对访问者公布细节,这是迪米特法则所不建议的。
- 具体元素变更比较困难:
- 具体元素角色的增加、删除、修改都是比较困难的。
- 违背了依赖倒置原则:访问者依赖的是具体元素,而不是抽象元素,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。
访问者模式的使用场景:
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景。
- 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。
访问者模式的扩展
- 统计功能
- 多个访问者
- 双分派
访问者模式的实例
- 大老板就看部门经理的报表,小兵的报表可看可不看。
- 多个大老板的“嗜好”是不同的,主管销售的,则主要关心营销的情况;主管会计的, 则主要关心企业的整体财务运行状态;主管技术的,则主要看技术的研发情况。
每一个员工的信息(如名字、性别、薪水等)都是记录在数据库中,根据这样的需求,把公司中的所有人员信息都打印汇报上去。
(1)访问者模式
★ 定义雇员类,其中包含所有雇员的公有属性,并定义允许访问者访问的方法accept()。
public abstract class Employee {
public final static int MALE = 0; //男性
public final static int FEMALE = 1; //女性
private String name;
private int salary;
private int sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
//允许一个访问者访问
public abstract void accept(IVisitor visitor);
}
★ 定义普通员工类,其中包括普通员工的特有属性,并实现雇员类,允许IVisitor接口下的访问者。
import com.sfq.impl.Employee;
import com.sfq.impl.IVisitor;
public class CommonEmployee extends Employee {
private String job;
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
★ 定义经理类,其中包括经理的特有属性,并实现雇员类,允许IVisitor接口下的访问者访问。
import com.sfq.impl.Employee;
import com.sfq.impl.IVisitor;
public class Manager extends Employee {
private String performance;
public String getPerformance() {
return performance;
}
public void setPerformance(String performance) {
this.performance = performance;
}
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
★ 定义IVisitor接口,设置访问的类。
import com.sfq.action.CommonEmployee;
import com.sfq.action.Manager;
public interface IVisitor {
//定义可以访问的对象
public void visit(CommonEmployee commonEmployee);
public void visit(Manager manager);
}
★ 定义一个真实的访问者,实现了IVisitor接口,并定义所要产生的逻辑结构。
import com.sfq.impl.Employee;
import com.sfq.impl.IVisitor;
public class Visitor implements IVisitor {
//打印普通员工报表
@Override
public void visit(CommonEmployee commonEmployee) {
System.out.println(getCommonEmployee(commonEmployee));
}
//打印部门经理报表
@Override
public void visit(Manager manager) {
System.out.println(getManagerInfo(manager));
}
//组装基本信息
private String getBasicInfo(Employee employee) {
String info = "姓名:" + employee.getName() + "\t";
info = info + "性别:" + (employee.getSex() == employee.FEMALE?"女":"男") + "\t";
info = info + "薪水:" + employee.getSalary();
return info;
}
//组装部门经理信息
private String getManagerInfo(Manager manager) {
String basicInfo = this.getBasicInfo(manager);
String otherInfo = "业绩:" + manager.getPerformance() + "\t";
return basicInfo + otherInfo;
}
//组装普通员工信息
private String getCommonEmployee(CommonEmployee commonEmployee) {
String basicInfo = this.getBasicInfo(commonEmployee);
String otherInfo = "工作:" + commonEmployee.getJob() + "\t";
return basicInfo + otherInfo;
}
}
★ 在场景类中进行员工信息组装和打印
import java.util.ArrayList;
import com.sfq.action.CommonEmployee;
import com.sfq.action.Manager;
import com.sfq.action.Visitor;
import com.sfq.impl.Employee;
public class Client {
public static void main(String[] args) {
for(Employee emp:mockEmployee()) {
emp.accept(new Visitor());
}
}
public static ArrayList<Employee> mockEmployee() {
ArrayList<Employee> empList = new ArrayList<Employee>();
//员工
CommonEmployee zhangSan = new CommonEmployee();
zhangSan.setJob("蓝领、苦工、搬运工!");
zhangSan.setName("张三");
zhangSan.setSalary(1000);
zhangSan.setSex(Employee.MALE);
empList.add(zhangSan);
//经理
Manager liSi = new Manager();
liSi.setPerformance("业绩为负,全靠舔!");
liSi.setName("李四");
liSi.setSalary(10000);
liSi.setSex(Employee.MALE);
empList.add(liSi);
return empList;
}
}
结果
姓名:张三 性别:男 薪水:1000工作:蓝领、苦工、搬运工!
姓名:李四 性别:男 薪水:10000业绩:业绩为负,全靠舔!
场景类中的mockEmployee()方法,实际上就是我们通用模型中的结构对象ObjectStruture。
如果想要修改报表格式,只需要再产生一个IVisitor的实现类就可以了。如果使用Spring开发,并且在Spring的配置文件中使用接口注入,则只需要把配置文件中的 ref 修改即可。
(2)统计功能
处理数据库中上亿条的数据,必须使用存储过程来处理,如果使用应用服务器,则会导致数据库连接始终处于100%占用状态。但对于少量的数据的统计和报表的批处理通过访问者模式来处理会比较简单。如下,统计一下公司人员的工资总额。
★ 在IVisitor接口中加入求工资总和的方法getTotalSalary()。
import com.sfq.action.CommonEmployee;
import com.sfq.action.Manager;
public interface IVisitor {
//定义可以访问的对象
public void visit(CommonEmployee commonEmployee);
public void visit(Manager manager);
public int getTotalSalary();
}
★ 在Visitor中分别定义普工和经理的工资系数,根据实际工资求得各自的工资,然后实现getTotalSalary()方法
public class Visitor implements IVisitor {
//部门经理工资系数
private final static int MANAGER_COEFFICIENT = 5;
//员工工资系数
private final static int COMMONEMPLOYEE_COEFFICIENT = 2;
//经理工资总和
private int managerTotalSalary = 0;
//普通员工工资总和
private int commonTotalSalary = 0;
//所有员工工资总和
@Override
public int getTotalSalary() {
return this.commonTotalSalary + this.managerTotalSalary;
}
//计算普工工资总和
private void calCommonSalary(int salary) {
this.commonTotalSalary = this.commonTotalSalary + salary * COMMONEMPLOYEE_COEFFICIENT;
}
//计算经理工资总和
private void calManagerSalary(int salary) {
this.managerTotalSalary = this.managerTotalSalary + salary * MANAGER_COEFFICIENT;
}
}
★ 在场景类中调用即可。当然,这个程序是需要结合数据库才能计算所有员工工资的。
public class Client {
public static void main(String[] args) {
IVisitor visitor = new Visitor();
for(Employee emp:mockEmployee()) {
emp.accept(visitor);
}
System.out.println("月工资总和:"+visitor.getTotalSalary());
}
}
(3)多个访问者
在实际的项目中,一个对象,多个访问者的情况非常多。
通常情况下,报表分两种:
- 展示表:通过数据库查询,把结果展示出来;
- 汇总表:需要通过模型或者公式计算,一般都是批处理,这类似于计算工资总额;
这两种报表格式是对同一堆数据的两种处理方式。从程序上看,一个类就有了不同的访问者了。
★ 展示表接口:
public interface IShowVisitor extends IVisitor {
//展示报表
public void report();
}
★ 具体展示表:
public class ShowVisitor implements IShowVisitor {
private String info = "";
//打印出报表
public void report() {
System.out.println(this.info);
}
//访问普通员工,组装信息
public void visit(CommonEmployee commonEmployee) {
this.info = this.info + this.getBasicInfo(commonEmployee) + "工作:"+commonEmployee.getJob()+"\t\n";
}
//访问经理,然后组装信息
public void visit(Manager manager) {
this.info = this.info + this.getBasicInfo(manager) + "业绩:"+manager.getPerformance() + "\t\n";
}
//组装出基本信息
private String getBasicInfo(Employee employee){
String info = "姓名:" + employee.getName() + "\t";
info = info + "性别:" + (employee.getSex() == Employee.FEMALE?"女":"男") + "\t";
info = info + "薪水:" + employee.getSalary() + "\t";
return info;
}
}
★ 汇总表接口:
public interface ITotalVisitor extends IVisitor {
//统计所有员工工资总和
public void totalSalary();
}
★ 具体汇总表:
public class TotalVisitor implements ITotalVisitor {
//部门经理的工资系数是5
private final static int MANAGER_COEFFICIENT = 5;
//员工的工资系数是2
private final static int COMMONEMPLOYEE_COEFFICIENT = 2;
//普通员工的工资总和
private int commonTotalSalary = 0;
//部门经理的工资总和
private int managerTotalSalary =0;
public void totalSalary() {
System.out.println("本公司的月工资总额是" + (this.commonTotalSalary +
this.managerTotalSalary));
}
//访问普通员工,计算工资总额
public void visit(CommonEmployee commonEmployee) {
this.commonTotalSalary = this.commonTotalSalary + commonEmployee.getSalary()*COMMONEMPLOYEE_COEFFICIENT;
}
//访问部门经理,计算工资总额
public void visit(Manager manager) {
this.managerTotalSalary = this.managerTotalSalary + manager.getSalary()*MANAGER_COEFFICIENT ;
}
}
★ 两个访问者的调用
public class Client {
public static void main(String[] args) {
//展示报表访问者
IShowVisitor showVisitor = new ShowVisitor();
//汇总报表的访问者
ITotalVisitor totalVisitor = new TotalVisitor();
for(Employee emp:mockEmployee()){
emp.accept(showVisitor); //接受展示报表访问者
emp.accept(totalVisitor);//接受汇总表访问者
}
//展示报表
showVisitor.report();
//汇总报表
totalVisitor.totalSalary();
}
}
(4)双分派
双分派(double dispatch)问题:双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型。
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 abstract class AbsActor {
//演员都能够演一个角色
public void act(Role role){
System.out.println("演员可以扮演任何角色");
}
//可以演功夫戏
public void act(KungFuRole role){
System.out.println("演员都可以演功夫角色");
}
}
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();
//开始演戏
role.accept(actor);
}
}
结果
年龄大了,不能演功夫角色
不管演员类和角色类怎么变化,我们都能够找到期望的方法运行,这就是双分派。