行为型模式之访问者模式(Java版)

设计模式概述及分类:https://blog.csdn.net/qq_34896730/article/details/105324092

面向对象设计原则:https://editor.csdn.net/md/?articleId=105352240

      访问者模式是一种较为复杂的行为型设计模式,它包含访问者和被访问元素两个主要组成部分,这些被访问的元素具有不同的类型,且不同的访问者可以对其施加不同的访问操作。访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。

1 访问者模式概述

      在医生开具处方单(药单)后,很多医院都存在以下处理流程:划价人员拿到处方单之后根据药品名称和数量计算总价,药房工作人员根据药品名称和数量准备药品。
      可以将处方单看成一个药品信息的集合,里面包含了一种或多种不同类型的药品信息,不同类型的工作人员(例如划价人员和药房工作人员)在操作同一个药品信息集合时将提供不同的处理方式,而且可能还会增加新类型的工作人员来操作处方单。
      在软件开发中有时候也需要处理像处方单这样的集合对象结构,在该对象结构中存储了多种不同类型的对象元素,而且对同一对象结构中的元素的操作方式并不唯一,可能需要提供多种不同的处理方式,还有可能增加新的处理方式。在设计模式中有一种模式可以满足上述需求,其模式动机就是以不同的方式操作复制对象结构,该模式就是访问者模式。
      访问者模式是一种较为复杂的行为型设计模式,它包含访问者和被访问元素两个主要组成部分,这些被访问的元素通常具有不同的类型,且不同的访问者可以对它们进行不同的访问操作。例如处方单中的各种药品信息就是被访问的元素,而划价人员和药房工作人员就是访问者。访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。
      在使用访问者模式时,被访问元素通常不是单独存在的,它们存储在一个集合中,这个集合被称为"对象结构",访问者通过遍历对象结构实现对其中存储元素的逐个操作。
      访问者模式的定义:表示一个作用于某对象结构中的各个元素的操作。访问者模式让用户可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
      访问者模式是一种对象行为型模式,它为操作存储不同类型元素的对象结构提供了一种解决方案,用户可以对不同类型的元素施加不同的操作。

2 访问者模式结构与实现

2.1 访问者模式结构

      访问者模式的结果较为复杂,其结构如图2-1所示。

图2-1 访问者模式结构图
      由图2-1可知,访问者模式包含以下5个角色。

      (1) Visitor(抽象访问者): 抽象访问者为对象结构中的每一个 具体元素类声明一个访问操作,从这个操作的名称或参数类型可以清楚地知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。
      (2) ConcreteVisitor(具体访问者): 具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中的一种类型的元素。。
      (3) Element(抽象元素): 抽象元素一般是抽象类或者接口,它声明了一个accept()方法用于接收访问者的访问操作,该方法通常以一个抽象访问者作为参数。
      (4) ConcreteElement(具体元素): 具体元素实现了accept()方法,在accept()方法中调用访问方法以便完成对一个元素的操作。
      (5) ObjectStructure(对象结构): 对象结构是一个元素的集合,它用于存放元素对象,并且提供了变量其内部元素的方法。对象结构可以结合组合模式来实现,也可以是一个简单的集合对象。

2.2 访问者模式实现

      在访问者模式中,对象结构存储了不同类型的元素对象,以供不同访问者访问。访问者模式包括两个层次结构,提供了抽象元素和具体元素。相同的访问者可以用不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同的方式访问。在访问者模式中增加新的访问者无须修改原有系统,系统具有较好的可扩展性。
      在访问者模式中,抽象访问者定义了访问元素对象的方法,通常为每一种类型的元素对象都提供一个访问方法,而具体访问者可以实现这些访问方法。这些访问方法的命名一般有两种方式:一种是直接在方法名中标明待访问元素对象的具体类型,例如visitElementA(ElementA elementA);另一种是统一命名为visit(),通过参数类型的不同来定义一系列重载的visit()方法。当然,如果所有的访问者对某一类型的元素的访问操作都相同,则可以将操作代码移到抽象访问者类中,其典型代码如下:

public abstract class Visitor{
    public abstract  void visit(ConcreteElementA elementA);
    public abstract  void visit(ConcreteElementB elementB);
    public void visit(ConcreteElementC elementC){
    //元素ConcreteElementC 操作代码
    }
}

      在这里使用了重载visit()方法的方式来定义多个方法,用于操作不同类型的元素对象。在抽象访问者Visitor的子类ConcreteVisitor中实现了抽象的访问方法,用于定义对不同类型元素对象的操作,具体访问者类的典型代码如下:

public class ConcreteVisitor extends Visitor{
    public void visit(ConcreteElementA elementA) {
        //元素ConcreteElementA 操作代码
    }
     public void visit(ConcreteElementB elementB) {
        //元素ConcreteElementB 操作代码
    }
}

      对于元素类而言,在其中一般都定义了一个accept()方法,用于接受访问者的访问。典型的抽象元素类的代码如下:

public interface Element{
    public void accept(Visitor visitor);
}

      需要注意的是,该方法传入了一个抽象访问者Visitor类型的参数,即针对抽象访问者对象的visit()方法,而不是具体访问者,在程序运行时再确定具体访问者的类型,并调用具体访问者对象的visit()方法实现对元素对象的操作。在抽象元素类Element的子类中实现了accept()方法,用于接受访问者的访问,在具体元素类中还可以定义不同类型的元素所特有的业务方法,其典型代码如下:

public class ConcreteElementA implement Element{
   public void accept(Visitor visitor){
      visitor.visit(this);
   }
   public void operationA(){
   //业务方法
   }
}

      在具体元素类ConcreteElementA的accept()方法中,通过调用Visitor类的visit()方法实现对元素的访问,并以当前对象作为visit()方法的操作。其具体执行过程如下:
      (1) 调用具体元素类的accept(Visitor visitor)方法,并将Visitor子类对象作为其参数。
      (2) 在具体元素类accept(Visitor visitor) 方法内部调用传入的Visitor对象的visit()方法,例如visit(ConcreteElementA elementA),将当前具体元素对象(this)作为参数,例如visitor.visit(this)。
      (3) 执行Visitor对象的visit()方法,在其中还可以调用具体元素的业务方法。
      这种调用机制也称为“双重分派”,正因为使用了双重分分派机制,使得增加新的访问者无须修改现有类库代码,只需将新的访问者对象作为参数传入具体元素的accept()方法,程序运行时将回调在新增Visitor类中定义的visit()方法,从而增加新的访问方式。
      在访问者模式中对象结构是一个集合,用于存储元素对象并接受访问者的访问。其典型代码如下:

public class ObjectStructure{
  //定义一个集合用于存储元素对象
  private ArrayList<Element> list = new ArrayList<>();
  //接受访问者的访问操作
  public void accept(Visitor visitor){
    Iterator i = list.iterator();
    while(i.hasNext()){
      //遍历访问集合中的每一个元素
      ((Element)i.next()).accept(visitor);
    }
  }
  public void addElement(Element element){
    list.add(element);
  }
   public void removeElement(Element element){
    list.remove(element);
  }
}

      在对象结构中可以使用迭代器对存储在集合中的元素对象进行遍历,并逐个调用每一个对象的accept()方法,实现对元素对象的访问操作。

3 访问者模式应用实例

      下面通过应用实例来进一步学习和理解访问者模式。
      1. 实例说明
      某软件公司OA系统中包含一个员工管理子系统,该公司员工包括正式员工和临时工,每周人员资源部和财务部门需要对员工数据进行汇总,汇总数据包括员工工作时间、员工工资等。该公司的基本制度如下:
      (1) 正式员工每周工作时间为40小时,不同几倍、不同部门的员工每周基本工资不同;如果超过40小时,超出部分按照100元/小时作为加班费;如果少于40小时,所缺部分按照请假处理,请假所扣工资以80元/小时计算,直到基本工资扣除为零为止。除了记录实际工作时间外,人力资源部需要记录加班时长或请假时长,作为员工平时表现的一种。
      (2) 临时工工作时间不固定,基本工资按小时计算,不同岗位的临时工小时工资不同。人力资源部只需要记录实际工作时间。。
      现使用访问者模式设计该系统,绘制类图并使用Java语言编码实现。
      2. 实例类图
      通过分析,本实例的结构图如图3-1所示。

图3-1 员工数据汇总模块结构图
      在图3-1中,FADepartment表示财务部,HRDepartment表示人力资源部,它们充当。

      3. 实例代码
      (1) Employee: 员工类,充当抽象元素类。

public interface Employee{
  public void accept(Department department);//接受一个抽象访问者访问
}

      (2) FulltimeEmployee:全职员工类,充当具体元素类。

public class FulltimeEmployee implements Employee{
  private String name;//员工姓名
  private double weeklyWage;//员工周薪
  private int workTime;//工作时间
  public FulltimeEmployee(String name,double weeklyWage,int workTime){
      this.name = name;
      this.weeklyWage = weeklyWage;
      this.workTime = workTime;
  }
  public void setName(String name){
      this.name = name;
  }
  public void setWeeklyWage(double weeklyWage){
      this.weeklyWage = weeklyWage;
  }
  public void setWorkTime(int workTime){
      this.workTime = workTime; 
  }
  public String getName(){
    return this.name;
  }
  public double getWeeklyWage(){
    return this.weeklyWage;
  }
  public int getWorkTime(){
    return this.workTime;
  }
  public void accept(Department handler){
    handler.visit(this);//调用访问者的方法
  }
}

      (3) ParttimeEmployee:兼职员工类,充当具体元素类。。

public class ParttimeEmployee implements Employee{
  private String name;//员工姓名
  private double hourWage;//员工时薪
  private int workTime;//工作时间
  public ParttimeEmployee(String name,double hourWage,int workTime){
      this.name = name;
      this.hourWage = hourWage;
      this.workTime = workTime;
  }
  public void setName(String name){
      this.name = name;
  }
  public void setHourWage(double hourWage){
      this.hourWage= hourWage;
  }
  public void setWorkTime(int workTime){
      this.workTime = workTime; 
  }
  public String getName(){
    return this.name;
  }
  public double getHourWage(){
    return this.hourWage;
  }
  public int getWorkTime(){
    return this.workTime;
  }
  public void accept(Department handler){
    handler.visit(this);//调用访问者的方法
  }
}

      (4) Department:部门类,充当抽象访问者类。

public abstract class Department{
 //声明一组重载的访问方法,用于访问不同类型的具体元素
 public abstract void visit(FulltimeEmployee employee);
 public abstract void visit(ParttimeEmployee employee);
}

      (5) FADepartment:财务部类,充当具体访问者类。

public class FADepartment implements Department{
 //实现财务部对全职员工的访问
  public void visit(FulltimeEmployee employee) {
    int workTime = employee.getWorkTime();
    double weekWage = employee.getWeeklyWage();
    if(workTime>40){
      weekWage = weekWage + (workTime - 40) * 100;
    }else(workTime<40){
      weekWage = weekWage - (40 - workTime) * 80;
      if(weekWage<0){
        weekWage  = 0;
      }
   }
   System.out.println("正式员工"+employee.getName()+"实际工资为:"+weekWage+"元");
  }
  //实现财务部对兼职员工的访问
  public void visit(ParttimeEmployee employee){
     int workTime = employee.getWorkTime();
     double hourWage = employee.getHourWage();
      System.out.println("临时工"+employee.getName()+"实际工资为:"+workTimew*hourWage+"元");
  }
}

      (6) HRDepartment:人力资源部,充当具体访问者类。

public class HRDepartment implements Department{
    //实现人力资源部对全职员工的访问
    public void visit(FulltimeEmployee employee){
      int workTime = employee.getWorkTime();
       System.out.println("正式员工"+employee.getName()+"实际工作时间为:"+workTime+"小时");
       if(workTime >40){
       System.out.println("正式员工"+employee.getName()+"加班时间为:"+(workTime+40)+"小时");
       }else if(workTime<40){
          System.out.println("正式员工"+employee.getName()+"请假时间为:"+(40-workTime)+"小时");
       }
    }
    //实现人力资源部对兼职员工的访问
    public void visit(FulltimeEmployee employee){
       int workTime = employee.getWorkTime();
       System.out.println("临时工"+employee.getName()+"实际工作时间为:"+workTime+"小时");
    }
}

      (7) EmployeeList:员工列表类,充当对象结构。

public class EmployeeList{
 //定义一个集合用于存储员工对象
 private ArrayList<Employee> list = new ArrayList<Employee>();
 public void addEmployee(Employee employee){
    list.add(employee);
 }
 //遍历访问员工集合中的每一个员工对象
 public void accpet(Department handler){
    for(Object obj:list){
       ((Employee)obj).accept(handler);
    }
 }
}

      (8) 配置文件config.xml,在配置文件中存储了具体访问者类的类名。

<?xml version="1.0" encoding="UTF-8" ?>
<config>
 <className>designpatterns.visitor.FADepartment</className>
</config>

      (9) XMLUtil:工具类。

public class XMLUtil {
  //该方法用于从XML配置文件中提取具体类的类名,并返回一个实例对象
  public static Object getBean(){
  try {
        //创建文档对象
        DocumentBuilderFactory dFactory=DocumentBuilderFactory.newInstance();
        DocumentBuilder builder= dFactory.newDocumentBuilder();
        Document doc;
        doc = builder.parse(new File("src//main//resources//config.xml"));
        //获取包含类名的文本节点
        NodeList nl = doc.getElementsByTagName("className");
        Node classNode = nl.item(0).getFirstChild();
        String cName = classNode.getNodeValue();
        //通过类名生成实例对象并将其返回
        Class c = Class.forName(cName);
        Object obj = c.newInstance();
        return obj;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
  }
}

      (10) Client:客户端测试类。

public class Client {
public static void main(String[] args) {
   EmployeeList list = new EmployeeList();
   Employee file1,file2,file3,pte1,pte2;
   file1=new FulltimeEmployee("张三",3200.00,45);
   file2=new FulltimeEmployee("李四",3000.00,40);
   file3=new FulltimeEmployee("王五",2400.00,38);
   pte1=new ParttimeEmployee("麻子",80.00,20);
   pte2=new ParttimeEmployee("王二",60.00,18);    
   list.addEmployee(file1);
   list.addEmployee(file2);
   list.addEmployee(file3);
   list.addEmployee(pte1);
   list.addEmployee(pte2);
   Department dep;
   dep = (Department)XMLUtil.getBean();
   list.accept(dep);
 }
}

      4. 结果及分析

正式员工张三实际工资为:3700.0元。
正式员工李四实际工资为:2000.0元。
正式员工王五实际工资为:2240.0元。
临时工麻子实际工资为:1600.0元。
临时工王二实际工资为:1080.0元。

      如果需要更换具体访问者类,无须修改源代码,只需修改配置文件即可。例如将访问者类由财务部改为人力资源部,只需要将存储在配置文件中的具体访问者类FADepartment给为HRDepartment。
      如果要在系统中增加一种新的访问者,无须修改源代码,只要增加一个新的具体访问者类即可,在该具体访问者中封装了新的操作元素对象的方法。从增加新的访问者的角度来看,访问者模式符合开闭原则。
      如果在系统中增加一种新的具体元素,例如增加一种新的员工类型为“退休人员”,由于原有系统并未提供相应的访问接口(在抽象访问者中没有声明任何访问“退休人员”的方法),因此必须对原有系统进行修改,在原有的抽象访问者类和具体访问者类中增加相应的访问方法。从增加新的元素的角度来看,访问者模式违背了开闭原则。
      综上所述,访问者模式与抽象工厂模式类似,对开闭原则的支持具有倾斜性,可以很方便地添加新的访问者,但是添加新的元素较为麻烦。

4 访问者模式与组合模式联用

      在访问者模式中包含一个用于存储元素对象集合的对象结构,通常可以使用迭代器来遍历对象结构,同时具体元素之间可以存在整体与部分关系,有些元素作为容器对象,有些元素作为成员对象,可以使用组合模式来组织元素。引入组合模式后的访问者模式结构图如图4-1所示。
在这里插入图片描述

图4-1 访问者模式与组合模式联用示意图
      需要注意的是,在图4-1所示的结构图中,由于叶子元素的遍历操作已经在容器中完成,因此要防止单独将已加载到容器元素中的叶子节点再次加入对象结构中,在对象结构中只需要保存容器元素和孤立的叶子元素。

5 访问者模式优/缺点与适用环境

5.1 访问者模式优点

      (1) 在访问者模式中增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合开闭原则。
      (2) 访问者模式将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。
      (3) 访问者模式让用户能够在不修改现有元素类层次结构的情况下定义作用于该层次结构的操作。

5.2 访问者模式缺点

      (1) 在访问者模式中增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都以为着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了开闭原则的要求。
      (2) 访问者模式破坏了对象的封装性。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这以为着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法提供访问者访问。

5.3 访问者模式适用环境

      (1) 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以对不同的访问操作。
      (2)需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得用户可以将相关的访问操作几种起来定义在访问者类中,对象结构可以被多个不同的访问者所使用,将对象本身与对象的访问操作分离。
      (3) 对象结构中对象对应的类很少改表,但经常需要在此对象结构上定义新的操作。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值