返回 导航

其他

hangge.com

Java - GoF设计模式详解23(访问者模式)

作者:hangge | 2023-07-07 09:08

二十三、访问者模式

1,基本介绍

(1)访问者模式(Visitor):将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使得在不改变各元素类的前提下定义作用于这些元素的新操作。访问者模式符合单一职责原则和开闭原则。

(2)该模式中包含的角色及其职责如下:
  • 抽象访问者角色Visitor):为该对象结构中具体元素角色声明一个访问操作接口,这些操作接口的名称和参数根据被访问的元素类型而异。
  • 具体访问者角色ConcreteVisitor):抽象访问者的实现类,实现了抽象访问者类中定义的操作。
  • 抽象元素角色Element): 抽象元素类,定义了一个接受访问者的方法,该方法的参数类型根据访问者而异。
  • 具体元素角色ConcreteElement):具体元素类,实现了抽象元素类中定义的接受访问者方法。

2,使用样例

(1)下面通组织结构中的员工的工资、假期统计样例来演示访问者模式的使用。一个公司组织结构,其中有员工和经理两种类型的成员。首先我们定义一个抽象元素接口 Element,里面声明了一个接受访问者的方法:
// 抽象元素
interface Element {
  // 接受访问者的方法
  void accept(Visitor visitor);
}

(2)EmployeeManager 类分别表示员工和经理,它们实现了 Element 接口并实现了接受访问者的方法。下面是员工类的代码:
// 员工类
class Employee implements Element {
  // 姓名
  private String name;

  // 薪水
  private double salary;

  // 假期天数
  private int vacationDays;

  public Employee(String name, double salary, int vacationDays) {
    this.name = name;
    this.salary = salary;
    this.vacationDays = vacationDays;
  }

  public String getName() {
    return name;
  }

  public double getSalary() {
    return salary;
  }

  public int getVacationDays() {
    return vacationDays;
  }

  // 接受访问者
  public void accept(Visitor visitor) {
    visitor.visit(this);
  }
}
// 经理类
class Manager implements Element {
  // 姓名
  private String name;

  // 薪水
  private double salary;

  // 假期天数
  private int vacationDays;

  // 所管理的员工
  private List<Employee> employees;

  public Manager(String name, double salary, int vacationDays) {
    this.name = name;
    this.salary = salary;
    this.vacationDays = vacationDays;
    employees = new ArrayList<>();
  }

  public String getName() {
    return name;
  }

  public double getSalary() {
    return salary;
  }

  public int getVacationDays() {
    return vacationDays;
  }

  public void addEmployee(Employee employee) {
    employees.add(employee);
  }

  public List<Employee> getEmployees() {
    return employees;
  }

  // 接受访问者
  public void accept(Visitor visitor) {
    visitor.visit(this);
  }
}

(3)然后定义一个 Visitor 接口,该接口声明了访问员工和经理的方法:
// 抽象访问者
interface Visitor {
  // 访问员工
  void visit(Employee employee);

  // 访问经理
  void visit(Manager manager);
}

(4)接着定义两个具体访问者:VacationVisitor 类和 SalaryVisitor 类,它们实现了 Visitor 接口并实现了访问员工和经理的方法,分别用于统计员工和经理的总假期天数、以及总工资。
// 假期统计访问者
class VacationVisitor implements Visitor {
  // 总假期
  private int totalVacationDays;

  // 访问员工
  public void visit(Employee employee) {
    totalVacationDays += employee.getVacationDays();
  }

  // 访问经理
  public void visit(Manager manager) {
    totalVacationDays += manager.getVacationDays();
    for (Employee employee : manager.getEmployees()) {
      employee.accept(this);
    }
  }

  public int getTotalVacationDays() {
    return totalVacationDays;
  }
}


// 工资统计访问者
class SalaryVisitor implements Visitor {
  // 总工资
  private double totalSalary;

  // 访问员工
  public void visit(Employee employee) {
    totalSalary += employee.getSalary();
  }

  // 访问经理
  public void visit(Manager manager) {
    totalSalary += manager.getSalary();
    for (Employee employee : manager.getEmployees()) {
      employee.accept(this);
    }
  }

  public double getTotalSalary() {
    return totalSalary;
  }
}

(5)最后测试一下,我们通过使用 VacationVisitor 类和 SalaryVisitor 类来分别计算整个公司的总假期天数以及总工资,而无需更改 EmployeeManager 类。
public class Test {
  public static void main(String[] args) {
    // 创建了2个员工和1个经理
    Employee liu = new Employee("小刘", 5000, 14);
    Employee li = new Employee("小李", 6000, 10);
    Manager wang = new Manager("王总", 10000, 7);

    // 将员工添加到经理中形成组织结构
    wang.addEmployee(liu);
    wang.addEmployee(li);

    // 示例化统计假期天数访问者类
    VacationVisitor vacationVisitor = new VacationVisitor();
    // 示例化统计工资访问者类
    SalaryVisitor salaryVisitor = new SalaryVisitor();

    // 调用公司经理的 accept 方法,传入访问者类对象
    wang.accept(vacationVisitor);
    wang.accept(salaryVisitor);

    // 输出统计的总假期天数和总工资
    System.out.println("总假期天数是: " + vacationVisitor.getTotalVacationDays());
    System.out.println("总工资是:" + salaryVisitor.getTotalSalary());
  }
}

附一:JDK 中的访问者模式

(1)JDKNIO 模块下的 FileVisitor 接口(java.nio.file.FileVisitor),它提供了递归遍历文件树的支持。这个接口上的方法表示了遍历过程中的关键过程,允许在文件被访问、目录将被访问、目录已被访问、发生错误等过程中进行控制。换句话说,这个接口在文件被访问前、访问中和访问后,以及产生错误的时候都有相应的钩子程序进行处理。

(2)通过访问者去遍历文件树会比较方便,比如查找文件夹内符合某个条件的文件或者某一天内所创建的文件。下面示例中,我们传入一个初始目录,使用 Files.walkFileTree 遍历这个目录,并打印文件名和目录名。我们继承了 SimpleFileVisitor 并重写了 preVisitDirectoryvisitFilepostVisitDirectory 三个方法,这样就可以在遍历目录之前,遍历文件时和遍历目录之后执行不同的操作。
FileVisitResult 枚举说明:
  • FileVisitResult.CONTINUE:这个访问结果表示当前的遍历过程将会继续。
  • FileVisitResult.SKIP_SIBLINGS:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点。
  • FileVisitResult.SKIP_SUBTREE:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前目录下的所有节点。
  • FileVisitResult.TERMINATE:这个访问结果表示当前的遍历过程将会停止。
public class Test {
  public static void main(String[] args) throws IOException {
    // 设置初始目录
    Path startingDir = Paths.get("/Volumes/BOOTCAMP/test");
    // 使用FileVisitor遍历文件系统中的文件和目录
    Files.walkFileTree(startingDir, new PrintNames());
  }
}

//自定义FileVisitor类
class PrintNames extends SimpleFileVisitor<Path> {
  //在遍历文件时执行
  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    System.out.println(file.getFileName());
    //继续遍历
    return FileVisitResult.CONTINUE;
  }

  //在遍历目录之前执行
  @Override
  public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    System.out.println(">>> 进入目录: " + dir.getFileName());
    //继续遍历
    return FileVisitResult.CONTINUE;
  }

  //在遍历目录之后执行
  @Override
  public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
    System.out.println("<<< 离开目录: " + dir.getFileName());
    //继续遍历
    return FileVisitResult.CONTINUE;
  }
}

附二:Spring 中的访问者模式

(1)Spring 中的 BeanDefinitionVisitor 类用于遍历和修改 bean 定义,这个就使用到了访问者模式。BeanDefinitionSpring Bean 的定义信息,在 Spring 解析完配置后,会生成 BeanDefinition 并且记录下来。下次通过 getBean 获取 Bean 的时候,会通过 BeanDefinition 来实例化具体的 Bean 对象。SpringBeanDefinitionVisitor 用来访问 BeanDefinition。主要在于解析属性或者构造方法里面的占位符,并且把解析的结果更新到 BeanDefinition 中。这里就应用的访问者模式。
<bean id="person" class="com.phoegel.visitor.analysis.Person" scope="prototype">
    <property name="name" value="${person.name}"/>
    <property name="age" value="${person.age}"/>
</bean>

(2)下面是 SpringPlaceholderConfigurerSupport类中的 doProcessProperties() 方法,在 doProcessProperties 内部就使用到了 BeanDefinitionVisitor 类,这个类就代表了访问者类。这里因为没有对访问者进行扩展,所以只有一个具体访问者 BeanDefinitionVisitor,没有再抽出一层抽象访问者。
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
                                   StringValueResolver valueResolver) {
  // 通过 BeanDefinitionVisitor 类的 visitBeanDefinition() 方法来实现访问者模式的核心思想
  BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);

  String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
  for (String curName : beanNames) {
    if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
      BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
      try {
        visitor.visitBeanDefinition(bd);
      }
      catch (Exception ex) {
        throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, 
                ex.getMessage(), ex);
      }
    }
  }
  beanFactoryToProcess.resolveAliases(valueResolver);
  beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
}

(3)而访问者的访问方法如下,可以发现 BeanDefinition 是一个接口,也就是访问者模式中的抽象元素角色,而它的子类有 RootBeanDefinitionChildBeanDefinitionGenericBeanDefinition 等等,这些可以理解为具体的元素角色。需要注意的是,这里的 BeanDefinition 明显是一个实现类,也就是说在 Spring 中并没有抽象出抽象访问者来对具体访问者类进行扩展,但是访问者模式的思想在上面几个类之间的运用得到了充分的体现。
public void visitBeanDefinition(BeanDefinition beanDefinition) {
  this.visitParentName(beanDefinition);
  this.visitBeanClassName(beanDefinition);
  this.visitFactoryBeanName(beanDefinition);
  this.visitFactoryMethodName(beanDefinition);
  this.visitScope(beanDefinition);
  if (beanDefinition.hasPropertyValues()) {
    this.visitPropertyValues(beanDefinition.getPropertyValues());
  }

  if (beanDefinition.hasConstructorArgumentValues()) {
    ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
    this.visitIndexedArgumentValues(cas.getIndexedArgumentValues());
    this.visitGenericArgumentValues(cas.getGenericArgumentValues());
  }

}
评论

全部评论(0)

回到顶部