访问者模式

访问者模式定义如下:封装一些作用于某种数据结构中各个元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

下面看一下几个抽象角色:

  • Visitor抽象访问者:抽象类或接口,定义访问者可以访问哪些元素。
  • ConcreateVisitor具体访问者:它影响访问者访问到一个类之后该怎么办,做哪些事情。
  • Element抽象元素:接口或抽象类,声明接收哪一类访问者访问,程序中通过accept方法中的参数来定义
  • ConcreateElement具体元素:实现accept方法,通常是visitor.visit(this),基本上都是一种固定形式了
  • ObjectStructure结构对象:元素产生者,一般容纳在多个不同类、不同接口的容器,如List Map等,在项目中,一般很少抽象出这个角色

看一下访问者模式的通用代码:

//抽象元素
public abstract class Element{
    //定义业务逻辑
    public abstract  void doSomething();
    //允许谁来访问
    public abstract void accept(IVisitor visitor);
}

//具体元素
public class ConcreateElement1 extends Element{
    //完善业务逻辑
    public void doSomething(){
        //业务逻辑
    }
    //允许哪个访问者来访问
    public void accept(IVisitor visitor){
        visitor.visit(this);
    }
}
public class ConcreateElement2 extends Element{
    //完善业务逻辑
    public void doSomething(){
        //业务逻辑
    }
    //允许哪个访问者来访问
    public void accept(IVisitor visitor){
        visitor.visit(this);
    }
}

再来看看抽象访问者,一般是有几个具体元素就有几个访问方法:

//抽象访问者
public interface IVisitor{
    //可以访问哪些对象
    public void visit(ConcreateElement1 el1);
    public void visit(ConcreateElement2 el2);
}
//具体访问者
public class Visitor implements IVIsitor{
    //访问el1元素
    public void visit(ConcreateElement1 el1){
        el1.doSomething();
    }
    //访问el2元素
    public void visit(ConcreateElement2 el2){
        el2.doSomething();
    }
}

结构对象是产生出不同的元素对象,这里使用工厂方法模式来模式:

//结构对象
public class ObjectStruture {
    //对象生成器, 这里通过一个工厂方法模式模拟
    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 el = ObjectStructure.createElement();
            //接收访问者访问
            el.accept(new Visitor());
        }
    }
}

通过增加访问者,只要是具体元素就非常容易访问,不管是什么对象,只要它在一个容器中,都可以通过访问者来访问,任务集中化,这就是访问者模式。

优点:

  • 符合单一职责原则:具体元素角色负责数据的加载,而Visitor类则负责展示数据,两个不同的职责各自分离开来,各自演绎变化。
  • 优秀的扩展性:由于职责分开,继续增加对数据的操作的非常方便快捷的,直接在visitor中增加一个响应的方法即可。
  • 灵活性非常高

缺点:

  • 具体元素对访问者公布细节:访问者要访问一个类就必然要求该类公共一些方法和数据,也就是说访问者关注了其他类的内部细节,这是迪米特法则不允许的。
  • 具体元素变更更困难:具体元素角色的增删改会比较麻烦,如果修改了这些,那它的访问者类也要修改,一个还好,要是有多个访问者呢?
  • 违背依赖倒置原则:访问者类依赖的是具体实现类,而不是抽象元素,在面向对象编程中,抛弃对接口的依赖,而直接依赖实现类,扩展会比较困难。

使用场景:

  • 业务规则要求遍历多个不同的对象,而迭代器模式只能访问同类或同接口的数据,作为对迭代器模式的扩充,遍历不同的对象,然后执行不同的操作
  • 充当拦截器的角色

访问者模式的扩展:

  • 统计功能

  比如金融系统中常用的汇总和报表功能,一堆的计算公式,然后出一堆报表,很多项目使用存储过程来实现,但不是很推荐,除非是海量数据,批处理上亿、上百亿条的数据,除了存储过程没有其他办法,如果用应用服务器,数据库连接将一直是100%的连接状态,如果不是这种海量数据,数据报表和统计功能使用访问者模式会更简单。

  下面看代码示例:

//抽象访问者
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 MANAGER_COEFFICIENT = 5;
    //员工的工资系数是2
    private final static int COMMONEMPLOYEE_COEFFICIENT = 2;
    //普通员工的工资总和
    private int commonTotalSalary = 0;
    //部门经理的工资总和
    private int managerTotalSalary =0;
    //计算部门经理的工资总和
    private void calManagerSalary(int salary){
        this.managerTotalSalary = this.managerTotalSalary + salary
        *MANAGER_COEFFICIENT ;
    }
    //计算普通员工的工资总和
    private void calCommonSlary(int salary){
        this.commonTotalSalary = this.commonTotalSalary +
        salary*COMMONEMPLOYEE_COEFFICIENT;
    }
    //获得所有员工的工资总和
    public int getTotalSalary(){
        return this.commonTotalSalary + this.managerTotalSalary;
    }
}

  员工和经理类暂时不列出了,程序很简单,现在看一个场景类:

//场景类
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());
    }
    //模拟出公司的人员情况, 我们可以想象这个数据是通过持久层传递过来的
    public static List<Employee> mockEmployee(){
      List<Employee> empList = new ArrayList<Employee>();
      //产生张三这个员工
      CommonEmployee zhangSan = new CommonEmployee();
      zhangSan.setJob("编写Java程序, 绝对的蓝领、 苦工加搬运工");
      zhangSan.setName("张三");
      zhangSan.setSalary(1800);
      zhangSan.setSex(Employee.MALE);
      empList.add(zhangSan);
      //产生李四这个员工
      CommonEmployee liSi = new CommonEmployee();
      liSi.setJob("页面美工, 审美素质太不流行了! ");
      liSi.setName("李四");
      liSi.setSalary(1900);
      liSi.setSex(Employee.FEMALE);
      empList.add(liSi);
      //再产生一个经理
      Manager wangWu = new Manager();
      wangWu.setName("王五");
      wangWu.setPerformance("基本上是负值, 但是我会拍马屁呀");
      wangWu.setSalary(18750);
      wangWu.setSex(Employee.MALE);
      empList.add(wangWu);
      return empList;
  } 
}
  • 多个访问者

  在实际项目中,一个对象,可能有多个访问者。比如上面报表的例子,报表分两种:第一种是展示表,通过数据库查询,把结果展示出来,类似于列表;第二种是汇总表,需要通过模型或公式计算出来的,一般都是批处理结果,比如计算工资平均值或总值,两种方式是对同一堆数据的不同处理方式。从程序上看,一个类就有了两个访问者了。下面看代码

//展示表接口
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()+"	
";
    }
    public void visit(Manager manager){
        this.info = this.info + this.getBasicInfo(manager) + "业绩:
"+manager.getPerformance() + "	
";
    }
    //组装出基本信息
    private String getBasicInfo(Employee employee){
        String info = "姓名: " + employee.getName() + "	";
        info = info + "性别: " + (employee.getSex() ==Employee.FEMALE?"女":"男") + "	";
        info = info + "薪水: " + employee.getSalary() + "	";
        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.getSal
    }
    //访问部门经理, 计算工资总额
    public void visit(Manager manager) {
        this.managerTotalSalary = this.managerTotalSalary +     manager.getSalary()
    }
}

  最后在场景类计算工资总和:

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();
    }
}
  • 双分派 

  什么是双分派?先解释下双分派和单分派。单分派语言处理一个操作是根据请求者的名称和接收到的参数决定的,在java中有动态绑定和静态绑定之说,它的实现是依据重载和覆写实现的。举个栗子, 演员演电影角色,一个演员可以演多个角色,我们先定义两个角色:功夫主角和白痴配角。代码如下:

//角色接口和实现类
public interface Role{
    //演员要扮演的角色
}
public class KungfuRole implements Role{
    //武功第一角色
}
public class IdiotRole implements Role{
    //白痴配角
}

//抽象演员 使用重载方式实现
public abstract class AbsActor{
    //演员都能演一个角色
    public void act(Role role){
    }
    //演一个功夫角色
    public void act(KungFuRole role){
    }
}

//使用覆写方式实现
public class YoungActor extends AbsActor{
    //年轻演员演功夫戏
    public void act(KungFuRole role){
    }
}
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(Rolerole)方法, 这是静态绑定; 而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是一个支持双分派的单分派语言。


原文地址:https://www.cnblogs.com/loveBolin/p/9752149.html