访问者模式详解

1.简介

在现实生活中,有些集合对象存在多种不同的元素,且每种元素也存在多种不同的访问者和处理方式。例如,公园中存在多个景点,也存在多个游客,不同的游客对同一个景点的评价可能不同;医院医生开的处方单中包含多种药元素,查看它的划价员和药房工作人员对它的处理方式也不同,划价员根据处方单上面的药品和数量进行划分,药房工作人员根据处方单的内容进行抓药。
这样的例子还有很多,例如,电影或电视剧中的人物角色,不同的观众对他们的评价也不同;还有顾客在商场购物时放在“购物车”中的商品,顾客主要关心所选商品的性价比,而收银员关心的是商品的价格和数量。
这些被处理的数据元素相对稳定而访问方式多种多样的数据结构,如果用“访问者模式”来处理比较方便。访问者模式能把处理方法从数据结构中分离出来,并可以根据需要增加的新的处理方法,且不用修改原来的程序代码与数据结构,这提高了程序的扩展性和灵活性。

2.定义

访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将数据的操作与数据结构进行分离。

3.优点

  1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  3. 灵活性好。访问者模式将数据结构与用作于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

4.缺点

  1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

5.结构

访问者(Visitor) 模式实现的关键是如何将作用于元素的操作分离出来封装成独立的类,其主要角色如下:

  1. 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作visit(),该操作中的参数类型表示了被访问的具体元素。
  2. 具体访问者(Concrete Visitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  3. 抽象元素(Element)角色:声明一个包含接受操作accept()操作,其方法体通常都是visitor.visit(this),另外具体元素中可能还包含业务本身逻辑的相关操作。
  4. 具体元素(Concrete Element)角色:实现抽象元素角色提供的accept()操作,其方法体通常都是visitor.visit(this),另外具体元素中可能还包含本身业务逻辑的相关操作。
  5. 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由List、Set、Map等聚合类实现。

结构图如下:
在这里插入图片描述

6.应用场景

当系统中存在类型数量稳定(固定)的一类数据结构时,可以使用访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会对数据产生任何副作用(脏数据)。
简而言之,就是当对集合中的不同类型数据(类型数量稳定)进行多种操作时,使用访问者模式。
通常在以下情况下可以考虑使用访问者(Visitor)模式:

  1. 对象结构相对稳定,但其操作算法经常变化的程序。
  2. 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
  3. 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。

7.代码样例

1.简单样例

/**
 * 抽象访问者
 */
interface Visitor{
    void visit(ConcreteElementA element);
    void visit(ConcreteElementB element);
}

/**
 * 具体访问者A类
 */
class ConcreteVisitorA implements Visitor{
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("具体访问者A访问->" + element.operationA());
    }

    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("具体访问者A访问->" + element.operationB());
    }
}

/**
 * 具体访问者B类
 */
class ConcreteVisitorB implements Visitor{
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("具体访问者B访问->" + element.operationA());
    }

    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("具体访问者B访问->" + element.operationB());
    }
}

/**
 * 抽象元素类
 */
interface Element{
    void accept(Visitor visitor);
}

/**
 * 具体元素A类
 */
class ConcreteElementA implements Element{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public String operationA(){
        return "具体元素A的操作";
    }
}

/**
 * 具体元素B类
 */
class ConcreteElementB implements Element{

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public String operationB(){
        return "具体元素B的操作";
    }
}

/**
 * 对象结构角色
 */
class ObjectStructure{
    private List<Element> list = new ArrayList<>();
    public void accept(Visitor visitor){
        Iterator<Element> i = list.iterator();
        while(i.hasNext()){
            i.next().accept(visitor);
        }
    }
    public void add(Element element){
        list.add(element);
    }
    public void remove(Element element){
        list.remove(element);
    }
}
public class VisitorPatternSimpleTest {
    public static void main(String[] args){
        ObjectStructure objectStructure = new ObjectStructure();
        objectStructure.add(new ConcreteElementA());
        objectStructure.add(new ConcreteElementB());

        Visitor visitorA = new ConcreteVisitorA();
        objectStructure.accept(visitorA);

        System.out.println("------------------------------");

        Visitor visitorB = new ConcreteVisitorB();
        objectStructure.accept(visitorB);
    }
}

2.应用实例

例:利用“访问者(Visitor)模式”模拟艺术公司与造币公司的功能。
分析:艺术公司利用“铜”可以设计出铜像,利用“纸”可以画出图画;造币公司利用“铜”可以印出铜币,利用“纸”可以印出纸币。对“铜”和“纸”这两种元素,两个公司的处理方法不同,所以该实例利用访问者模式来实现比较适合。
首先,定义一个公司(Company)接口,它是抽象访问者,提供了两个根据纸(Paper)或铜(Cuprum)这两种元素创建作品的方法;再定义艺术公司(ArtCompany)类和造币公司(Mint)类,他们是具体访问者,实现了父接口的方法。
然后,定义一个材料(Material)接口,它是抽象元素,提供了accept(Company visitor)方法来接受访问者(Company)对象访问:再定义纸(Paper)类和铜(Cuprum)类,它们是具体元素类,实现了父接口中的方法。
最后,定义一个材料集(SetMaterial)类,它是对象结构角色,拥有保存所有元素的容器List,并提供让访问者对象遍历容器中的所有元素的accept(Company visitor)方法;客户类提供材料集(SetMaterial)对象供访问者(Company)对象访问,实现了ItemListener接口,处理用于的事件请求。
结构图如下:
在这里插入图片描述
代码样例:

/**
 * 抽象访问者
 */
interface Company{
    String create(Paper element);
    String create(Cuprum element);
}

/**
 * 具体访问者
 */
class ArtCompany implements Company{
    @Override
    public String create(Paper element) {
        return "降雪图";
    }

    @Override
    public String create(Cuprum element) {
        return "朱熹铜像";
    }
}
class Mint implements Company{
    @Override
    public String create(Paper element) {
        return "纸币";
    }

    @Override
    public String create(Cuprum element) {
        return "铜币";
    }
}
/**
 * 抽象元素
 */
interface Material{
    String accept(Company visitor);
}

/**
 * 具体元素
 */
class Paper implements Material{
    @Override
    public String accept(Company visitor) {
        return visitor.create(this);
    }
}
class Cuprum implements Material{
    @Override
    public String accept(Company visitor) {
        return visitor.create(this);
    }
}

/**
 * 对象结构角色
 */
class SetMaterial{
    private List<Material> list = new ArrayList<>();
    public String accept(Company visitor){
        Iterator<Material> i = list.iterator();
        String tmp = "";
        while(i.hasNext()){
            tmp += i.next().accept(visitor) + " ";
        }
        return tmp;
    }
    public void add(Material element){
        list.add(element);
    }
    public void remove(Material element){
        list.remove(element);
    }
}
public class VisitorProducer {
    public static void main(String[] args){
        SetMaterial material = new SetMaterial();
        material.add(new Paper());
        material.add(new Cuprum());

        Company artCompany = new ArtCompany();
        System.out.println(material.accept(artCompany));

        System.out.println("----------");
        Company mint = new Mint();
        System.out.println(material.accept(mint));

    }
}

3.访问者模式的扩展

访问者模式是使用频率较高的一种设计模式,它常常同以下两种设计模式联用。
1.与“迭代器模式”联用。因为访问者模式中的“对象结构”是一种包含元素角色的容器,当访问者遍历容器中的所有元素时,常常要用到迭代器,如上述例子中的对象结构是用List实现的,它通过List对象的Iterator()方法获取迭代器。如果对象结构中的聚合类没有提供迭代器,也可以用迭代器模式自定义一个。

2.访问者模式同“组合模式”联用。因为访问者模式中的“元素对象”可能是叶子对象或者是容器对象,如果元素对象包含容器对象,就必须用到组合模式,其结构图如下:
在这里插入图片描述
代码样例:

interface Visitor{
    void visit(LeafElement element);
    void visit(CompositeElement element);
}
class ConcreteVisitorA implements Visitor{
    @Override
    public void visit(LeafElement element){
        System.out.println("访问者A对叶子节点进行操作!");
    }

    @Override
    public void visit(CompositeElement element) {
        System.out.println("访问者A对树枝节点进行操作!");
    }
}
class ConcreteVisitorB implements Visitor{
    @Override
    public void visit(LeafElement element) {
        System.out.println("访问者B对叶子节点进行操作!");
    }

    @Override
    public void visit(CompositeElement element) {
        System.out.println("访问者B对树枝节点进行操作!");
    }
}
interface Element{
    void accept(Visitor visitor);
}
class LeafElement implements Element{
    @Override
    public void accept(Visitor visitor){
        visitor.visit(this);
    }
    public String operationA(){
        return "叶子节点操作!";
    }
}
class CompositeElement implements Element{
    private List<Element> list = new ArrayList<>();
    @Override
    public void accept(Visitor visitor){
        visitor.visit(this);
        for(Element element : list){
            element.accept(visitor);
        }
    }
    public String operationB(){
        return "树枝节点操作!";
    }
    public void add(Element element){
        list.add(element);
    }
    public void remove(Element element){
        list.remove(element);
    }
    public Element getChild(int i){
        return list.get(i);
    }
}
class ObjectStructure{
    private List<Element> list = new ArrayList<>();
    public void accept(Visitor visitor){
        for(Element element : list){
            element.accept(visitor);
        }
    }
    public void add(Element element){
        list.add(element);
    }
    public void remove(Element element){
        list.remove(element);
    }
}
public class VisitorAndCompositeTest {
    public static void main(String[] args){
        Element leaf = new LeafElement();
        CompositeElement composite = new CompositeElement();
        composite.add(leaf);

        ObjectStructure objectStructure = new ObjectStructure();
        objectStructure.add(composite);

        Visitor visitorA = new ConcreteVisitorA();
        objectStructure.accept(visitorA);

        System.out.println("-----------------");

        Visitor visitorB = new ConcreteVisitorB();
        objectStructure.accept(visitorB);
    }
}

4.访问者模式的伪动态双分派

在访问者模式中使用的就是伪动态双分派。

1.分派的概念

变量被声明时的类型叫做变量的静态类型(Static Type),静态类型又叫做明显类型(Apparent Type)。而变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。
例如:

List list = null;
lis = new ArrayList();

上面代码声明了一个变量list,它的静态类型是List,而他的实际类型是ArrayList。根据对象类型对方法进行选择,就是分派(Dispatch)。分派又分两种,即静态分派和动态分派。

2.静态分派

静态分派(Static Dispatch)就是按照变量的静态类型进行分派,从而确定方法的执行版本,即所谓的编译时多态,发生在编译器。方法重载就是静态分派最典型的应用。
例如:

public class StaticDispatchTest {
    public void test(String string){
        System.out.println("string");
    }
    public void test(Integer integer){
        System.out.println("integer");
    }
    public static void main(String[] args){
        String string = "1";
        Integer integer = 1;
        StaticDispatchTest test = new StaticDispatchTest();
        test.test(string);
        test.test(integer);
    }
}

静态分派判断时,根据多个判断依据(即参数类型和个数)判断出方法的版本,这就是多分派的概念。因为我们有一个以上的考量标准,所以Java是静态多分派的语言。

3.动态分派

动态分派与静态分派相反,它发生在运行时期,运行时根据参数的类型,选择合适的重载方法,即所谓的运行时多态。多态就是动态分派最典型的应用。
例如:

interface Animal{
    void show();
}

class Dog implements Animal{
    @Override
    public void show() {
        System.out.println("小狗");
    }
}
class Cat implements Animal{
    @Override
    public void show() {
        System.out.println("小猫");
    }
}
public class DynamicDispatchTest {
    public static void main(String[] args){
        Animal dog = new Dog();
        Animal cat = new Cat();
        dog.show();
        cat.show();
    }
}

这里的show()方法无法根据Dog和Cat的静态类型判断,因为他们的静态类型都是Animal接口。显然,show()方法的版本是在运行时判断的,这就是动态分派。
动态分派判断的方法是在运行时获取Dog和Cat的实际引用类型,在确定方法的版本,而由于此时判断的依据只是实际引用类型,只有一个判断依据,所以这就是单分派的概念,这考量标准只有一个,即变量的实际应用类型。相应地,这说明Java是动态单分派语言。
通过前面的分析,我们知道Java是静态多分派、动态单分派的语言。Java底层不支持动态双分派,但是通过设计模式,也可以在Java里实现伪动态双分派。

4.伪动态双分派

在访问者模式中使用的就是伪动态双分派。所谓动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是使用两个动态单分派来达到这个效果。
在上面的ObjectStructure类中的accept()方法代码如下:

public void accept(Visitor visitor) {
    Iterator<Element> i=list.iterator();
    while(i.hasNext()) {
        i.next().accept(visitor);
    }     
}

这里根据Visitor和Element两个元素的实际类型来决定accept()方法的执行版本。
accept()方法的调用过程分析如下:

  1. 当调用accept()方法时,首先根据Element的实际类型决定是调用ConcreteElementA还是ConcreteElementB的accept()方法。
  2. 这是accept()方法的版本已经确定,假设是ConcreteElementA,则它的accept()方法调用以下代码:
public void accept(Visitor visitor) {
    visitor.visit(this);
}

此时的this是ConcreteElementA类型,因此对应的是Visitor接口的visit(ConcreteElementA element)方法,此时需要再根据访问者的实际类型确定visit()方法的版本(ConcreteElementA 还是 ConcreteElementB),如此一来,就完成了动态双分派的过程。
以上过程通过两次动态分派,第一次对accept()方法进行动态分派,第二次对访问者的visit()方法进行动态分派,从而达到根据两个世纪类型确定一个方法的行为效果。
而原本的做法通常是传入一个接口,直接使用接口的方法,此为动态单分派,就像是策略模式一样。而在这里,accept()方法传入的访问者接口并不是直接调用自己的visit()方法,而是通过Element的实际类型先动态分派一次,然后在分派后确定的方法版本里进行自己的动态分派。
注意:这里确定accept(Visitor visitor)方法是由静态分派决定的,所以这个并不在此次动态双分派的范畴内,而且静态分派是在编译器完成的,所以accept(Visitor visitor)方法的静态分派与访问者模式的动态双分派并没有任何关系。动态双分派说到底还是动态分派,是在运行时发生的,他与静态分派有着本质上的区别,不可以说一次动态分派加一次静态分派就是动态双分派,而且访问者模式的双分派本身也另有所指。
this的类型不是动态分派确定的,把它写在哪个类中,静态类型就是哪个类,这是在编译期就确定的,不确定的是它的实际类型。

5.访问者模式在JDK源码中的应用

在早期的Java版本中,如果要对指定目录下的文件进行遍历,必须用递归的方式来实现,这种方法复杂且灵活性不高。
Java 7版本后,Files类提供了walkFileTree()方法,该方法可以很容易地对目录下的所有文件进行遍历,需要Path、FileVisitor两个参数。其中,Path是要遍历文件的路径,FileVisitor则可以看成一个文件访问器。
源码如下:

package java.nio.file;
public final class Files {
    ...
    public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
            throws IOException
    {
        return walkFileTree(start,
                            EnumSet.noneOf(FileVisitOption.class),
                            Integer.MAX_VALUE,
                            visitor);
    }
    ...
}

FileVisitor提供了递归遍历文件树的支持,这个接口的方法表示了遍历过程中的关键过程,允许在文件被访问,目录将被访问、目录已被访问、发生错误等过程中进行控制。换句话说,这个接口在文件被访问前、访问中和访问后,以及产生错误的时候都有相应的钩子程序进行处理。
FileVisitor主要提供了4个方法,且返回结构都是FileVisitorResult对象值,用于决定当前操作完成后接下来该如何处理。
FileVisitResult是一个枚举类,代表返回之后的一些后续操作。
源码如下:

package java.nio.file;

import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
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;
}

package java.nio.file;

public enum FileVisitResult {
   
    CONTINUE,
   
    TERMINATE,
   
    SKIP_SUBTREE,
   
    SKIP_SIBLINGS;
}

FileVisitResult主要包含4个常见的操作。
1.FileVisitResult.CONTINUE:表示当前的遍历过程将会继续。
2.FileVisitResult.SKIP_SIBLINGS:表示当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点。
3.FileVisitResult.SKIP_SUBTREE:表示当前的遍历将会继续,但是要忽略当前目录下的所有节点。
4.FileVisitResult.TERMINATE:表示当前的遍历过程将会停止。
通过访问者去遍历文件数会比较方便,比如查找文件夹内符合某个条件的文件或者某一天所创建的文件,这个类中都提供了相对应的方法。它的实现也非常简单:

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;
    }
}

6.访问者模式在Spring源码中的应用

在Spring的IOC中,BeanDefinition用来存储Spring Bean的定义信息,比如属性值、构造方法参数或者更具体的实现。Spring解析完配置后,会生成BeanDefinition并且记录下来。下次getBean获取Bean时,会通过BeanDefinition来实例化具体的Bean对象。
BeanDefinition是一个接口,即一个抽象的定义,实际使用的是其实现类,如ChildBeanDifinition、RootBeanDefinition、GenericBeanDifinition等。
Spring中的BeanDefinitionVisitor类主要用于访问BeanDefinition,解析属性或者构造方法里面的占位符,并把解析结果更新到BeanDefinition中,这里应用的就是访问者模式。抽象元素为BeanDefinition,具体元素为RootBeanDefinition、ChildBeanDefinition、GenericBeanDefinition等。
因为没有对访问者进行扩展,所以这里只有一个具体访问者BeanDefinitionVisitor,没有再抽出一层抽象访问者。
BeanDefinitionVisitor的源码如下:

public class BeanDefinitionVisitor {
    @Nullable
    private StringValueResolver valueResolver;

    public BeanDefinitionVisitor(StringValueResolver valueResolver) {
        Assert.notNull(valueResolver, "StringValueResolver must not be null");
        this.valueResolver = valueResolver;
    }

    protected BeanDefinitionVisitor() {
    }

    public void visitBeanDefinition(BeanDefinition beanDefinition) {
        visitParentName(beanDefinition);
        visitBeanClassName(beanDefinition);
        visitFactoryBeanName(beanDefinition);
        visitFactoryMethodName(beanDefinition);
        visitScope(beanDefinition);         
        if (beanDefinition.hasPropertyValues()) {
             visitPropertyValues(beanDefinition.getPropertyValues());
        }
        if (beanDefinition.hasConstructorArgumentValues()) {
            ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
            visitIndexedArgumentValues(cas.getIndexedArgumentValues());
            visitGenericArgumentValues(cas.getGenericArgumentValues());
        }
    }
    protected void visitParentName(BeanDefinition beanDefinition) {
        String parentName = beanDefinition.getParentName();
        if (parentName != null) {
            String resolvedName = resolveStringValue(parentName);
            if (!parentName.equals(resolvedName)) {
                beanDefinition.setParentName(resolvedName);
            }
        }
    }
    ...
} 

以上没有使用双重分派模式,直接调用visit进行元素的访问。visitBeanDefinition()方法分别实现了不同的visit来对相应的数据进行不同的处理。我们看到,在visitBeanDefiniton()方法总,访问了其他数据,比如父类的名字、自己的类名、在IOC容器中的名称等各种信息。

8.思维导图

请添加图片描述


版权声明:本文为d303577562原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。