乐趣区

关于设计模式:大白话聊访问者模式从入门到实践

文章首发于集体博客 shuyi.tech,欢送拜访更多乏味有价值的文章。

访问者模式,重点在于访问者二字。说到拜访,咱们脑海中必定会想起新闻访谈,两个人面对面坐在一起。从字面上的意思了解:其实就相当于被访问者(某个公众人物)把访问者(记者)当成了外人,不想你轻易动。你想要什么,我弄好之后给你(调用你的办法)。

01 什么是访问者模式?

访问者模式的定义如下所示,说的是在不扭转数据结构的提前下,定义新操作。

封装一些作用于某种数据结构中的各元素的操作,它能够在不扭转数据结构的前提下定义作用于这些元素的新的操作。

但在理论的利用中,我发现有些例子并不是如此。有些例子中并没有稳固的数据结构,而是稳固的算法。 在树义看来,访问者模式是:把不变的固定起来,变动的凋谢进来。

咱们举生存中一个例子来聊聊:某科学家承受记着访谈。咱们都晓得科学家承受拜访,必定是有流程上的限度的,不可能让你轻易问。咱们假如这个过程是:先问科学家的学校经验,再聊你的工作经验,最初聊你的科研成果。那么在这个过程中,固定的是什么货色呢?固定的是承受采访的流程。变动的是什么呢?变动的是不同的记者,针对学校经验,可能会提不同的问题。

依据咱们之前的了解,访问者模式其实就是要把不变的货色固定起来,变动的凋谢进来。那么对于科学家承受访谈这个事件,咱们能够这么将其抽象化。

首先,咱们须要有一个 Visitor 类,这里定义了一些内部(记者)能够做的事件(提学校经验、工作经验、科研成就的问题)。

public interface Visitor {public void askSchoolExperience(String name);
    public void askWorkExperience(String name);
    public void askScienceAchievement(String name);
}

接着申明一个 XinhuaVisitor 类去实现 Visitor 类,这示意是新华社的一个记者(访问者)想去拜访科学家。

public class XinhuaVisitor implements Visitor{
    @Override
    public void askSchoolExperience(String name) {System.out.printf("请问 %s:在学校获得的最大成就是什么?\n", name);
    }

    @Override
    public void askWorkExperience(String name) {System.out.printf("请问 %s:工作上最难忘的事件是什么?\n", name);
    }

    @Override
    public void askScienceAchievement(String name) {System.out.printf("请问 %s:最大的科研成果是什么?", name);
    }
}

接着申明一个 Scientist 类,表明是一个科学家。科学家通过一个 accept() 办法接管记者(访问者)的拜访申请,将其存储起来。科学家定义了一个 interview 办法,将拜访的流程固定死了,只有教你问什么的时候,我才会让你(记者)发问。

public class Scientist {

    private Visitor visitor;

    private String name;

    private Scientist(){}

    public Scientist(String name) {this.name = name;}

    public void accept(Visitor visitor) {this.visitor = visitor;}

    public void interview(){System.out.println("------------ 拜访开始 ------------");
        System.out.println("--- 开始聊学校经验 ---");
        visitor.askSchoolExperience(name);
        System.out.println("--- 开始聊工作经验 ---");
        visitor.askWorkExperience(name);
        System.out.println("--- 开始聊科研成果 ---");
        visitor.askScienceAchievement(name);
    }
}

最初咱们申明一个场景类 Client,来模仿访谈这一过程。

public class Client {public static void main(String[] args) {Scientist yang = new Scientist("杨振宁");
        yang.accept(new XinhuaVisitor());
        yang.interview();}
}

运行的后果为:

------------ 拜访开始 ------------
--- 开始聊学校经验 ---
请问杨振宁:在学校获得的最大成就是什么?--- 开始聊工作经验 ---
请问杨振宁:工作上最难忘的意见事件是什么?--- 开始聊科研成果 ---
请问杨振宁:最大的科研成果是什么?

看到这里,大家对于访问者模式的实质有了更理性的意识(把不变的固定起来,变动的凋谢进来)。在这个例子中,不变的固定的就是访谈流程,变动的就是你能够提不同的问题。

一般来说,访问者模式的类构造如下图所示:

  • Visitor 访问者接口。访问者接口定义了访问者能够做的事件。这个须要你去剖析哪些是可变的,将这些可变的内容形象成访问者接口的办法,凋谢进来。而被访问者的信息,其实就是通过访问者的参数传递过来。
  • ConcreteVisitor 具体访问者。具体访问者定义了具体某一类访问者的实现。对于新华社记者来说,他们更关怀杨振宁迷信成绩方面的事件,于是他们发问的时候更偏向于开掘成绩。但对于青年报记者来说,他们的读者是青少年,他们更关怀杨振宁在学习、工作中的那种精力。
  • Element 具体元素。这里指的是具体被拜访的类,在咱们这个例子中指的是 Scientist 类。个别状况下,咱们会提供一个 accept() 办法,接管访问者参数,将相当于承受其范文申请。但这个办法也不是必须的,只有你可能拿到 visitor 对象,你怎么定义这个参数传递都能够。

对于访问者模式来说,最重要的莫过于 Visitor、ConcreteVisitor、Element 这三个类了。Visitor、ConcreteVisitor 定义访问者具体能做的事件,被访问者的参数通过参数传递给访问者。Element 则通过各种办法拿到被访问者对象,罕用的是通过 accept() 办法,但这并不是相对的。

须要留神的是,咱们学习设计模式重点是了解类与类之间的关系,以及他们传递的信息。至于是通过什么形式传递的,是通过 accept() 办法,还是通过构造函数,都不是重点。

02 访问者模式的理论利用

后面咱们用一个生存的例子帮忙大家了解访问者模式,置信大家对访问者模式应该有了个理性的了解了。为了回归编程实际自身,让大家对访问者模式能有更好的实际了解。上面咱们将从软件编程上讲讲访问者模式在开源框架中的利用。

文件树遍历

JDK 中有文件操作,咱们天然是分明的。有文件操作,那天然就会有文件夹的遍历操作,即拜访某个文件夹上面的所有文件或文件夹。试想一下,如果咱们想要打印出某个文件夹下所有文件及文件夹的名字,咱们须要怎么做?

很简略的做法,其实就是间接做一个树的遍历,而后将名字打印进去呀!

没错,这的确是正确答案!

那么如果我心愿统计一下所有文件及文件夹的个数呢?

那就再遍历一次,而后用一个计数器去始终加一呗!

没错,这也是正确答案!

但你是否发现了这两个过程中,咱们有一个雷同的操作:遍历文件树。无论是打印文件名,还是计算文件树,咱们都须要去遍历文件树。而无论哪一个过程,咱们最终要的其实就是拜访文件。

还记得咱们说过设计模式的实质是什么吗? 设计模式的实质是找出不变的货色,再找出变动的货色,而后找到适合的数据结构(设计模式)去承载这种变动。

在这个例子里,不变的货色是文件树的遍历,变动的是对于文件的不同拜访操作。很显然,访问者模式是比拟适宜承载这种变动的。咱们能够把这种不变的货色(文件树的遍历)固定起来,把变动的货色(文件的具体操作)凋谢进来。JDK 对于文件树的遍历,其实就是应用访问者模式实现的。

JDK 中申明了一个 FileVisitor 接口,定义了遍历者能够做的操作。

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

FileVisitor 中定义的 visitFile() 办法,其实就是对于文件的拜访。被访问者(文件)的信息通过第一个参数 file 传递过去。这样遍历者就能够拜访文件的内容了。

SimpleFileVisitor 则是对于 FileVisitor 接口的实现,该类中仅仅是做了简略的参数校验,并没有太过的逻辑。

public class SimpleFileVisitor<T> implements FileVisitor<T> {
    @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;
    }
    //.... 其余省略
}

FileVisitor 类和 SimpleFileVisitor 类对应的就是 UML 类图中的 Visitor 和 ConcreteVisitor 类。而 Element 元素,对应的其实是 JDK 中的 Files 类。

Files 文件中遍历文件树是通过 walkFileTree() 办法实现的。在 walkFileTree() 办法中实现了树的遍历,在遍历到文件的时候会通过 visitor 类的 visitFile 办法调用遍历者的办法,将遍历到的文件传递给遍历者,从而达到拆散变动的目标。

ASM 批改字节码

ASM 是 Java 的字节码加强技术,这外面就用到了访问者模式,次要是用来进行字节码的批改。在 ASM 中于此相干的三个类别离是:ClassReader、ClassVisitor、ClassWriter。

ClassReader 类相当于访问者模式中的 Element 元素。它将字节数组或 class 文件读入内存中,并以树的数据结构示意。该类定义了一个 accept 办法用来和 visitor 交互。

ClassVisitor 相当于形象访问者接口。ClassReader 对象创立之后,须要调用 accept() 办法,传入一个 ClassVisitor 对象。在 ClassReader 的不同期间会调用 ClassVisitor 对象中不同的 visit() 办法,从而实现对字节码的批改。

ClassWriter 是 ClassVisitor 的是实现类,它负责将批改后的字节码输入为字节数组。

对于 ASM 这种场景而言,字节码标准是十分严格且稳固的,如果轻易更改可能出问题。但咱们又须要对字节码进行动静批改,从而达到某些目标。在这种状况下,ASM 的设计者采纳了访问者模式将变动的局部隔离开来,将不变的局部固定下来,从而达到了灵便扩大的目标。

03 咱们该如何应用?

从下面几个例子,咱们大抵能够明确访问者模式的应用场景: 某些较为稳固的货色(数据结构或算法),不想间接被扭转但又想扩大性能,这时候适宜用访问者模式。

说到对于访问者模式应用场景的定义,咱们会感觉模板办法模式与这个应用场景的定义很像。但它们还是有些许差异的。 访问者模式的变动与非变动(即访问者与被访问者)之间,它们只是简略的蕴含关系,而模板办法模式的变动与非变动则是继承关系。 但它们也的确有相似的中央,即都是封装了固定不变的货色,凋谢了变动的货色。

访问者模式的长处很显著,即隔离了变动的货色,固定了不变的货色,使得整体的可维护性更强、具备更强的扩展性。但它也带来了设计模式通用的一些毛病,例如:

  • 类构造变得复杂。之前咱们可是简略的调用关系,当初则是多个类之间的继承和组合关系。从肯定水平上,进步了对开发人员的要求,进步了研发老本。
  • 被访问者的变更变得更加艰难。例如咱们下面科学家访谈的例子,如果科学家访谈心愿新增一个环节,那么 Scientist 类须要批改,Visitor 类、XinhuaVisitor 类都须要批改。

有这些多长处,但也有这么多毛病,那理论工作中咱们应该怎么判断是否用访问者模式呢?

总的准则就是取长补短,即当场景齐全利用了访问者模式的长处,躲避了访问者模式的毛病的时候,就是应用访问者模式的最佳时机。

尽管应用访问者模式会让被访问者的变更变得更加艰难,但如果被访问者很稳固,根本不会变更,那这个毛病不就去除了么。例如在 ASM 的例子中,元素是 ClassReader,其存储了字节码的构造。而字节码构造齐全不会轻易扭转,所以在这个「被访问者的变更变得更加艰难」的毛病也就不存在了。

而「类构造变得复杂」这个毛病,则是须要依据过后业务的复杂程度来看的。如果过后业务很简略,而且变动也不大,那么应用设计模式齐全是多余的。然而如果过后业务很简单了,咱们还是在一个类里做批改,那么很大可能性会出大问题。这时候就须要用设计模式来承载简单的业务构造了。

04 参考资料

  • 一文说透访问者模式 – 犀牛饲养员博客
  • 访问者设计模式
  • 访问者模式一篇就够了 – 简书
  • 一文说透访问者模式 – 犀牛饲养员博客
  • 访问者模式在 ASM 框架中的应用
  • 访问者模式由浅入深及用例场景 加上 AMS 的简略应用_a1032722788 的博客 – CSDN 博客
退出移动版