visitor

第二十五章 访问者模式(Visitor Pattern)

1 概要

访问者模式(Visitor Pattern)是一种行为模式(Behavior Design Patters),它能帮助开发人员在对象的定义或者对象结构上新增操作或者逻辑,而不需要改变对象的结构。一个大型系统可能使用了许多对象结构(Object Structures)。这些对象结构包括各种对象容器(Collections),对象的继承关系(Inheritance Structure)或者其他复杂的对象关系。当需要向这些对象添加新的操作时,这个修改过程可能会变得非常复杂。所以,在此时,开发人员可以考虑使用访问者模式。

2 访问者模式的结构

访问者模式中有4个参与方。

  1. Element定义了受访问对象的接口。该接口接受一个Visitor对象作为参数。
  2. ConcreteElement表示具体的受访问对象,它实现了Element接口。
  3. Visitor定义了访问者接口。
  4. ConcreteVisitor表示具体的访问者,它实现了Visitor接口。

图一 访问者模式结构

图一 访问者模式结构。

3 访问者模式示例

我们通过一个访问加法表达式的访问者模式示例来介绍访问者模式的使用方法。我们首先创建一个Visitor接口,它用于处理表达式中的常量元素(Literal)和加法表达式(Addition)。ExpressionVisitor是Visitor接口的一个实现,它仅将收到的元素打印出来。

public interface Visitor {
    public void printLiteral (Literal literal);
    public void printAddition (Addition addition);
}

public class ExpressionVisitor implements Visitor {
    @Override
    public void printLiteral (Literal literal) {
        System.out.println("This is a literal");
    }
    
    @Override
    public void printAddition (Addition addition) {
        System.out.println("This is an addition expression");
    }
}

在第二步中,我们定义了表达式的结构和元素。Expression是表达式的接口,它只有一个成员方法accept(),用于接收一个访问者Visitor。Literal和Addition分别用于表示常量表达式和加法表达式。

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

public class Literal implements Expression {
    public void accept(Visitor visitor) {
        visitor.printLiteral(this);
    }
}

public class Addition implements Expression {
     public void accept(Visitor visitor) {
        visitor.printAddition(this);
    }
}

最后,我们在VisitorExample类中,创建了Visitor对象。并且在创建加法表达式1+2的过程中,分别调用了accept()方法两次,分别应用于一个常量对象和一个加法表达式对象。因此,Visitor对象会根据对象的类型,分别调用相应的处理方法。

public class VisitorExample {
    public static void main(String[] args) {
        Visitor visitor = new ExpressionVisitor();
        
        // 创建两个常量
        Literal l1 = new Literal(1);
        Literal l2 = new Literal(2);
        l1.accept(visitor); // 访问l1
        
        // 创建一个加法表达式 1+2
        Addition anExpression = new Addition(l1, l2);
        anExpression.accept(visitor); // 访问整个表达式
    }
}

4 应用示例

在Java标准库中有多处使用了访问者模式。我们将在本小节简要的介绍一下java.nio.file.FileVisitor和java.nio.file.SimpleFileVisitor的使用方法。更多的用例出现了Java编译器中,例如:javax.lang.model.element.AnnotationValueVisitor, javax.lang.model.element.ElementVisitor和javax.lang.model.type.TypeVisitor。有兴趣的读者可自行查阅其源代码。

java.nio.file.FileVisitor的一个常见的使用场景是递归遍历某一目录下所有的文件和目录。在java.nio.file.Files文件中,walkFileTree()方法会遍历给定目录下的所有文件和目录,并使用访问者模式运行用户指定的逻辑。因此,用户只需要在FileVisitor的子类中实现对于每个文件或者目录的逻辑即可。如下面的代码所示。

以下代码用于打印/tmp目录下所有文件的路径。Files.walkFileTree()会访问该路径下的所有文件,并回调SimpleFileVisitor对象中相应的方法。因此,开发人员可以在SimpleFileVisitor中实现相应的处理逻辑。在这个例子中,我们仅打印出文件的路径。

import java.util.EnumSet;
import java.nio.file.*;
import java.io.IOException;

public class PrintAllFilePaths {
    public static void main(String[] args) {
        final Path source = "/tmp";

        // 递归遍历/tmp目录下所有的文件
        Files.walkFileTree(source, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
            new SimpleFileVisitor<Path>() { // 实现一个匿名类,继承自SimpleFileVisitor
               @Override
               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                   // 当访问一个新的文件时,打印其路径
                   System.out.println(file.toString());
                   return CONTINUE;
               }
           });
    }
}

在Files.walkFileTree()方法内部,FileTreeWalker对象会遍历指定目录下的所有文件和目录。FileTreeWalker的实现使用了一个较为复杂的迭代器模式。更具体的讲,FileTreeWalker内部会创建多个迭代器对象;每访问完一个文件之后,会指向下一个文件。当进入更深一级的目录后,FileTreeWalker会创建出一个新的迭代器对象,访问该目录中的所有文件。

从FileTreeWalker的使用方式来看,变量walker实际上是一个迭代器对象,只不过从名字上看不出来而已。每当调用一次walker.next()之后,该迭代器会向前移动一个元素,直到访问完所有元素。当遇到文件对象时,会调用visitor.visitFile()接口,用于回调开发人员指定的逻辑。

// OpenJDK 15
package java.nio.file;

public final class Files {
    public static Path walkFileTree(Path start,
                                    Set<FileVisitOption> options,
                                    int maxDepth,
                                    FileVisitor<? super Path> visitor) throws IOException {
        try (FileTreeWalker walker = new FileTreeWalker(options, maxDepth)) {
            FileTreeWalker.Event ev = walker.walk(start);
            do {
                FileVisitResult result;
                switch (ev.type()) {
                    case ENTRY :
                        IOException ioe = ev.ioeException();
                        if (ioe == null) {
                            assert ev.attributes() != null;
                            result = visitor.visitFile(ev.file(), ev.attributes());
                        } else {
                            result = visitor.visitFileFailed(ev.file(), ioe);
                        }
                        break;

                    case START_DIRECTORY :
                        ...
                        break;

                    case END_DIRECTORY :
                        ...
                        break;

                    default :
                        throw new AssertionError("Should not get here");
                }

                ...
                ev = walker.next();
            } while (ev != null);
        }
        return start;
    }
}

最后,我们来看看java.nio.file.FileVisitor的实现。FileVisitor是一个接口,它定义了四个成员方法。在本例中,我们感兴趣的是visitFile()方法。

// OpenJDK 15
package java.nio.file;

public interface FileVisitor<T> {
    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException;
	FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException;
	FileVisitResult visitFileFailed(T file, IOException exc) throws IOException;
	FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;
}

SimpleFileVisitor是一个简单的实现类,实现了FileVisitor接口。SimpleFileVisitor并没有实质的逻辑;它仅在每个方法中检查了输入参数的合法性。因此,当使用FileVisitor时,开发人员需要实现一个SimpleFileVisitor类的子类,并在子类中完成感兴趣的方法。不感兴趣的方法可以不用处理,因为SimpleFileVisitor已经实现了一份空白的方法。

SimpleFileVisitor的构造函数是protected的,这意味着SimpleFileVisitor对象只能通过子类创建,开发人员不能直接创建一个SimpleFileVisitor对象。

// OpenJDK 15
package java.nio.file;

public class SimpleFileVisitor<T> implements FileVisitor<T> {
    protected SimpleFileVisitor() {
    }
	
	@Override
    public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException
    {
        Objects.requireNonNull(dir);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }
	
	@Override
    public FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException
    {
        Objects.requireNonNull(file);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }
	
	@Override
    public FileVisitResult visitFileFailed(T file, IOException exc) throws IOException
    {
        Objects.requireNonNull(file);
        throw exc;
    }
	
	@Override
    public FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException
    {
        Objects.requireNonNull(dir);
        if (exc != null)
            throw exc;
        return FileVisitResult.CONTINUE;
    }
}

5 小结

本章介绍了访问者模式的结构和使用方法。访问者模式能够在不改变原有对象结构的基础上新增处理逻辑和功能。这种设计能将对象的等级结构和其相应的处理分离开,降低代码的复杂度。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.