当前位置: 首页 > 科技观察

大白话说访客模型:从入门到练习

时间:2023-03-15 00:32:00 科技观察

访客模型,重点都在访客这个词上。当我们想到采访时,我们一定会想到新闻采访,两个人面对面坐着。从字面理解:其实就相当于受访者(公众人物)把采访者(记者)当外人,不想让你随便动。你要什么,我搞定了就给你(调用你的方法)。01什么是访客模式?访问者模式的定义如下,就是在不事先改变数据结构的情况下定义新的操作。将作用于某个数据结构中每个元素的一些操作封装起来,它可以在不改变数据结构的情况下定义作用于这些元素的新操作。但是在实际应用中,我发现有些例子并不是这样的。有些例子不是稳定的数据结构,而是稳定的算法。在树一看来,访客模式就是:固定不变,打开变化。说一个生活中的例子:科学家接受记者采访。我们都知道,科学家接受采访,肯定有流程限制,不可能你随便问。我们假设流程是:先问科学家的求学经历,然后说你的工作经历,最后说你的科研成果。那么在这个过程中,什么是固定的呢?固定的是面试流程。发生了什么变化?不同的是,不同的记者可能会就学校经历提出不同的问题。按照我们之前的理解,访客模式其实就是固定不变的东西,开放变化的东西。那么对于被采访的科学家这件事,我们可以这样抽象出来。首先,我们需要有一个Visitor类,定义了外部(记者)可以做的一些事情(就学经历、工作经历、科研成果提问)。publicinterfaceVisitor{publicvoidaskSchoolExperience(Stringname);publicvoidaskWorkExperience(Stringname);publicvoidaskScienceAchievement(Stringname);}然后声明一个XinhuaVisitor类来实现Visitor类,意思是新华社的记者(访客)要拜访科学家。publicclassXinhuaVisitorimplementsVisitor{@OverridepublicvoidaskSchoolExperience(Stringname){System.out.printf("请问%s:在学校最大的成就是什么?\n",name);}@OverridepublicvoidaskWorkExperience(Stringname){System.out.printf("Excuse我%s:工作中最难忘的事情是什么?\n",name);}@OverridepublicvoidaskScienceAchievement(Stringname){System.out.printf("请问%s:最大的科学成就是什么?",name);}}然后声明一个Scientist类,表示是科学家。科学家通过accept()方法收到记者(访问者)的采访申请并存储。科学家定义了一种采访方法来固定采访过程。只有我教你问什么,我才会让你(记者)提问。publicclassScientist{privateVisitorvisitor;privateStringname;privateScientist(){}publicScientist(Stringname){this.name=name;}publicvoidaccept(Visitorvisitor){this.visitor=visitor;}publicvoidinterview(){System.out.println("------------访问开始------------");System.out.println("---开始谈学校经历---");visitor.askSchoolExperience(name);System.out.println("---开始谈工作经历---");visitor.askWorkExperience(name);System.out.println("---开始谈科研成果---");visitor.askScienceAchievement(name);}}最后声明一个场景类Client来模拟面试过程。publicclassClient{publicstaticvoidmain(String[]args){Scientistyang=newScientist("杨振宁");yang.accept(newXinhuaVisitor());yang.interview();}}运行结果为:------------面试开始----------------开始说学校经历---请问杨振宁:在学校最大的收获是什么?---开始谈工作经历---杨振宁:工作中最难忘的事情是什么?---开始说说科研成果---请问杨振宁:最大的科研成果是什么?看到这里,大家对visitor模型的本质有了更感性的认识(固定不变的,开放改变的)。在这个例子中,不变的是面试流程,变化的是可以问不同的问题。总的来说,访问者模式的类结构如下图所示:Visitor访问者接口。访问者界面定义了访问者可以做什么。这就需要你去分析哪些是变量,把这些变量的内容抽象成访问者接口的一个方法,并开放出来。访问者的信息实际上是通过访问者的参数传递的。ConcreteVisitor具体访问者。具体访问者定义了特定类型访问者的实现。对于新华社记者来说,他们更关心杨振宁的科研成果,所以在提问的时候,更倾向于挖掘成果。但对于青年报记者来说,他们的读者是青少年,更关心的是杨振宁在学习和工作中的精神状态。元素混凝土元素。这是指被访问的特定类,在我们的例子中是科学家类。一般情况下,我们会提供一个accept()方法来接收访问者参数,相当于接受它的示例应用。但是这个方法不是必须的,只要你能拿到visitor对象,你可以定义这个参数以任何方式传递。对于访问者模式,三个最重要的类是Visitor、ConcreteVisitor和Element。Visitor和ConcreteVisitor定义了访问者可以做的具体事情,将visitor的参数通过参数传递给访问者。Element通过各种方法获得访问者的对象,常用的是通过accept()方法,但这不是绝对的。需要注意的是,我们学习设计模式的重点是了解类之间的关系以及它们传递的信息。至于通过什么方式传递,是通过accept()方法还是通过构造函数,不是重点。02访客模式的实际应用前面我们用一个生活例子来帮助大家理解访客模式。相信大家对访客模式应该有一个感性的认识。为了回归到编程实践本身,让大家对访问者模式有一个更好的实践认识。下面我们将从软件编程的角度来谈谈访客模式在开源框架中的应用。文件树遍历JDK中有文件操作,我们自然清楚。有文件操作,自然会有文件夹遍历操作,即访问某个文件夹下的所有文件或文件夹。试想一下,如果我们要打印出某个文件夹下所有文件和文件夹的名称,需要怎么做呢?很简单,其实就是直接做一个树遍历,然后把名字打印出来!没错,这确实是正确答案!那如果我想统计所有文件和文件夹的个数呢?然后再遍历一遍,然后用计数器一直加一!是的,这也是正确答案!但是你有没有发现,在这两个过程中,我们有相同的操作:遍历文件树。无论是打印文件名还是计算文件树,我们都需要遍历文件树。而无论是哪个进程,我们最终想要的都是访问文件。还记得我们说过的设计模式的本质吗?设计模式的本质就是找出什么是不变的,然后找出什么是变化的,然后找到一个合适的数据结构(设计模式)来承载这种变化。在这个例子中,不变的是文件树的遍历,变化的是对文件的不同访问操作。显然,访客模式更适合承载这种变化。我们可以固定这个不变的东西(文件树的遍历),打开改变的东西(文件的具体操作)。JDK对文件树的遍历实际上是使用访问者模式实现的。JDK中声明了一个FileVisitor接口,定义了遍历可以做的操作。publicinterfaceFileVisitor{FileVisitResultpreVisitDirectory(Tdir,BasicFileAttributesattrs);FileVisitResultvisitFile(Tfile,BasicFileAttributesattrs)throwsIOException;FileVisitResultvisitFileFailed(Tfile,IOExceptionexc)throwsIOException;FileVisitResultpostVisitDirectory(Tdir,IOExceptionexc)throwsIOException;}FileVisitor中定义的visitFile()方法,其实就是对于文件使用权。访问者(文件)的信息通过第一个参数文件传递。这允许遍历器访问文件的内容。SimpleFileVisitor是FileVisitor接口的实现。这个类只做简单的参数验证,没有过多的逻辑。publicclassSimpleFileVisitorimplementsFileVisitor{@OverridepublicFileVisitResultpreVisitDirectory(Tdir,BasicFileAttributesattrs)throwsIOException{Objects.requireNonNull(dir);Objects.requireNonNull(attrs);returnFileVisitResult.CONTINUE;}@OverridepublicFileVisitResultvisitFile(Tfile,BasicFileVisitResultVisitFile(Tfile,BasicFileRowAttributesAttrs)throwsIOException{Objects.requireNonNull(dir);Objects.requireNonNull(属性);}file);Objects.requireNonNull(attrs);returnFileVisitResult.CONTINUE;}//...其他省略}FileVisitor类和SimpleFileVisitor类分别对应UML类图中的Visitor和ConcreteVisitor类。Element元素对应于JDK中的Files类。遍历Files文件中的文件树是通过walkFileTree()方法实现的。遍历树是在walkFileTree()方法中实现的。遍历文件时,会通过访问者类的visitFile方法调用访问者的方法,将遍历后的文件传递给遍历器,从而达到分离变化的目的。ASM修改字节码ASM是Java的字节码增强技术,采用访问者模式,主要用于修改字节码。ASM中与此相关的三个类是:ClassReader、ClassVisitor、ClassWriter。ClassReader类相当于访问者模式中的Element元素。它将字节数组或类文件读入内存并将其表示为树数据结构。此类定义了一个与访问者交互的accept方法。ClassVisitor相当于抽象访问者接口。创建ClassReader对象后,需要调用accept()方法,传入一个ClassVisitor对象。ClassVisitor对象中不同的visit()方法会在ClassReader的不同阶段被调用,实现对字节码的修改。ClassWriter是ClassVisitor的实现类,负责将修改后的字节码输出为字节数组。对于ASM这样的场景,字节码规范是非常严格和稳定的,随便改动可能会出问题。但是我们需要动态修改字节码来达到某些目的。在这种情况下,ASM的设计者采用访问者模式,隔离变化的部分,固定不变的部分,从而达到灵活扩展的目的。03我们如何使用它?从上面的例子,我们可以大致了解访问者模式的使用场景:一些比较稳定的东西(数据结构或者算法),不想直接改动,想扩展功能,此时适合使用访问者模式图案。说到访问者模式的使用场景定义,我们会觉得模板方法模式和这种使用场景的定义非常相似。但它们仍然略有不同。访问者模式的变化与不变化(即访问者与访问者)之间只是简单的包含关系,而模板方法模式的变化与不变化是继承关系。但它们确实有相似之处,就是都封装了固定的东西,开放了变化的东西。访问者模式的优势很明显,就是隔离了变化的东西,修复了不变化的东西,使得整体的可维护性和扩展性更强。但它也带来了一些设计模式共有的缺点,例如:类结构变得复杂。之前我们是简单的调用关系,现在是多个类之间的继承组合关系。一定程度上提高了对开发者的要求,增加了研发成本。更换受访者变得更加困难。比如我们上面科学家采访的例子,如果要添加科学家采访的链接,那么就需要修改Scientist类,Visitor类和XinhuaVisitor类都需要修改。优点有很多,但是缺点也有那么多,那么在实际工作中我们应该如何判断是否使用访客模式呢?总的原则是扬长避短,即当场景充分利用访客模式的优点,避免访客模式的缺点时,就是使用访客模式的最佳时机。虽然使用visitor模式会让visitor变的比较困难,但是如果visitor是稳定的,基本不会变,那么这个缺点也改不掉。例如,在ASM的情况下,元素是一个ClassReader,它存储字节码的结构。字节码结构根本不会轻易改变,所以“改变被访问对象变得更难”的缺点是不存在的。“类结构变得复杂”的缺点需要根据当时业务的复杂程度来看待。如果当时的业务很简单,变化不大,那么用设计模式就完全多余了。但是如果当时业务很复杂,我们还是在一个类里面改,那很有可能出大问题。这时候就需要用设计模式来承载复杂的业务结构。本文转载自微信公众号“陈淑仪”,可通过以下二维码关注。转载本文请联系陈淑仪公众号。