理解访问者模式

763 阅读5分钟

前言

访问者模式使用的场景不少,但是实际使用的并不是很多,比较难理解是一个方面,这篇主要记录下访问者模式相关的学习内容,通过本文你可以了解到

  1. 什么是访问者模式,访问者模式想要解决的问题是什么?
  2. 访问者模式的经典应用有哪些?

什么是访问者模式?

很多设计模式文档给在介绍访问者模式时给了一个很抽象的定义,很容易让人摸不到头脑。不妨先从访问者模式的适用场景出发,来看下访问者模式想要解决的是什么问题。 在介绍设计模式之前,先了解几个基础的概念。了解概念的含义并不是为了咬文嚼字,而是希望能从原理上理解设计模式背后想要解决的问题和其含义。

在Java的多态,重写(override)和重载(overload)

  • 重写,就是子类重写了父类的方法,返回值和形参都不能改变。当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。
  • 重载,在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

什么是分派,单分派和双重分派

  • 分派(Dispatch),在面向对象的语言中,可以把一次函数调动理解成一个消息事件的分发,如a.test(b),a就是消息的接受者,这个函数的调用方就是消息的发送者。
  • 单分派(Single Dispatch),这里的单(Single)指的是,哪个对象的方法会被执行,只跟这个对象的运行时类型有关。以a.test(b)为例,如在Java中,在被执行的test函数,只跟a对象的运行时类型有关。
  • 双重分派(Double Dispatch),这里的双(double)指的是,哪个对象的方法被执行,跟对象和方法参数的运行时类型都有关。还是以a.test(b)为例,哪个test函数被执行,不单单和a对象的类型有关还和b对象的类型有关。

可以看到所谓分派就是函数的调用,所谓单分派和双分派就是和语言的多态特性有关,在常见的Java,C++,C#语言中,在语言层面都是只支持单分派的。想要实现双重分派,就要借助设计模式,比如访问者模式。你肯定会问,双重分派的作用是什么?不解决双重分派的问题不行吗?其实这种问题在项目代码中一定俯拾皆是,类似下面的这种代码,我们想要针对不同类型的文件(pdf,ppt,word)执行不同的文件提前和文件压缩操作。试想下,可能要怎么处理?

class Extractor extends Processor {
	void processFile(ResourceFile file) {
	    if (file instanceof PdfFile) {
	        processPrdFile((PdfFile)file);
	    } else if (file instanceof PowerPointFile) {
	        processPowerPointFile((PowerPointFile)e);
	    } else if (file instanceof WordFile) {
	        processWordFile((WordFile)e);
	    }
    }
}

class Compressor extends Processor {
	void processFile(ResourceFile file) {
	    if (file instanceof PdfFile) {
	        processPrdFile((PdfFile)file);
	    } else if (file instanceof PowerPointFile) {
	        processPowerPointFile((PowerPointFile)e);
	    } else if (file instanceof WordFile) {
	        processWordFile((WordFile)e);
	    }
    }
}

是不是很容易写出这样的代码(这里省略了部分抽象类和抽象方法的定义)?可以看到这段代码的逻辑执行,既要根据接收者的运行时类型来决定processXXXFile(file)的执行,这里的接收者可以理解成是当前方法所对应的Processor对象。又要根据ResourceFile file对象的运行时的实际类型来做类型的判断,这里就会有很多instanceofelse if,switch case的多重嵌套。这种设计的代码虽然可以实现功能,但是在面对需求变更和扩展时会非常不灵活,既要加很多else if,也不利于功能的内聚和复用。

这里就可以利用访问者模式来解决这样的双重分派问题,如上面的类图所示,通过几个角色来做功能的区分,将文件的访问和处理分离成两个独立的接口。

image.png

  • 访问者(Visitor),这里指不同类型的文件操作。用一个接口和一组不同类型的具体实现来定义不同的操作类型。
  • 被访问者(Element),这里指不同类型的文件。定义了accept操作,以Visitor作为参数,来接受不同类型visitor的对象访问。在accept方法中将this传递给访问者,通过回调再回调的操作,实现了双重分派。
  • 对象结构(ObjectStructure),访问的组织者,可以是组合也可以是集合;能够枚举它包含的元素;提供一个接口,允许Vistor访问它的元素。

这里借用王争在<<设计模式之美>>课程中的实例代码:

public abstract class ResourceFile {
  protected String filePath;
  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }
  abstract public void accept(Visitor vistor);
}

public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }

  @Override
  public void accept(Visitor visitor) {
    visitor.visit(this);
  }

  //...
}
//...PPTFile、WordFile跟PdfFile类似,这里就省略了...

public interface Visitor {
  void visit(PdfFile pdfFile);
  void visit(PPTFile pdfFile);
  void visit(WordFile pdfFile);
}

public class Extractor implements Visitor {
  @Override
  public void visit(PPTFile pptFile) {
    //...
    System.out.println("Extract PPT.");
  }

  @Override
  public void visit(PdfFile pdfFile) {
    //...
    System.out.println("Extract PDF.");
  }

  @Override
  public void visit(WordFile wordFile) {
    //...
    System.out.println("Extract WORD.");
  }
}

public class Compressor implements Visitor {
  @Override
  public void visit(PPTFile pptFile) {
    //...
    System.out.println("Compress PPT.");
  }

  @Override
  public void visit(PdfFile pdfFile) {
    //...
    System.out.println("Compress PDF.");
  }

  @Override
  public void visit(WordFile wordFile) {
    //...
    System.out.println("Compress WORD.");
  }

}

public class ToolApplication {
  public static void main(String[] args) {
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);

    Extractor extractor = new Extractor();
    for (ResourceFile resourceFile : resourceFiles) {
      resourceFile.accept(extractor);
    }

    Compressor compressor = new Compressor();
    for(ResourceFile resourceFile : resourceFiles) {
      resourceFile.accept(compressor);
    }
  }

  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

ASM中的访问者模式

上面介绍了访问者模式设计初衷和设计方法,这里再看下访问者模式在实际工程中的应用。访问者模式最常见的应用场景就是访问复杂的结构或者对象,在不改变数据结构的情况下,将数据访问和数据操作分离出来,用回调的方式在访问者中处理业务逻辑。在面对不同的访问处理时,只需要新定义一个访问者实现不同的访问处理逻辑就可以了。这样说可能也很抽象,可以在在ASM中,是如何利用访问者的设计模式,实现字节码文件的读取和修改的。 ASM使用ClassReader遍历class文件结构获取文件中的类和对象信息,在其accept方法中接收ClassVisitor,在ClassVisitor的不同回调方法中完成不同的字节码操作。可以通过代码示例看到主要有以下几个类:

ClassReader cr = new ClassReader(inputStream);
ClassWriter cw = new ClassWriter(cr, 0);

ClassVisitor cv = new InjectCassVisitor(ASM6, cw, methodName);
cr.accept(cv, 0);
return cw.toByteArray();
  • ClassReader(Element):将不同输入类型的字节码读取到内存中,通过accept方法接受ClassVisitor的访问。
  • ClassVisitor(Visitor):完全由开发者自定义不同类型的Visitor,在Visitor的visitXXX回调中接收读取到的字节码信息并进行相应的处理。

总结

理解访问者模式的设计,我觉得重点在理解所谓的回调再回调。这里有两次回调就意味着有两种类型接口,第一次回调是被访问者通过accept接受访问者,第二次回调是访问者通过visit方法访问被访问者,通过两次互相调换类型的调用,也就是通过两次单分派实现了双重分派。

参考文档

王争<<设计模式之美>>

www.jianshu.com/p/cd17bae4e… www.liaoxuefeng.com/wiki/125259…