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)Employee 和 Manager 类分别表示员工和经理,它们实现了 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 类来分别计算整个公司的总假期天数以及总工资,而无需更改 Employee 和 Manager 类。
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)JDK 的 NIO 模块下的 FileVisitor 接口(java.nio.file.FileVisitor),它提供了递归遍历文件树的支持。这个接口上的方法表示了遍历过程中的关键过程,允许在文件被访问、目录将被访问、目录已被访问、发生错误等过程中进行控制。换句话说,这个接口在文件被访问前、访问中和访问后,以及产生错误的时候都有相应的钩子程序进行处理。
(2)通过访问者去遍历文件树会比较方便,比如查找文件夹内符合某个条件的文件或者某一天内所创建的文件。下面示例中,我们传入一个初始目录,使用 Files.walkFileTree 遍历这个目录,并打印文件名和目录名。我们继承了 SimpleFileVisitor 并重写了 preVisitDirectory、visitFile、postVisitDirectory 三个方法,这样就可以在遍历目录之前,遍历文件时和遍历目录之后执行不同的操作。
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 定义,这个就使用到了访问者模式。BeanDefinition 为 Spring Bean 的定义信息,在 Spring 解析完配置后,会生成 BeanDefinition 并且记录下来。下次通过 getBean 获取 Bean 的时候,会通过 BeanDefinition 来实例化具体的 Bean 对象。Spring 的 BeanDefinitionVisitor 用来访问 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)下面是 Spring 中 PlaceholderConfigurerSupport类中的 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 是一个接口,也就是访问者模式中的抽象元素角色,而它的子类有 RootBeanDefinition、ChildBeanDefinition 和 GenericBeanDefinition 等等,这些可以理解为具体的元素角色。需要注意的是,这里的 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)