• 欢迎访问 winrains 的个人网站!
  • 本网站主要从互联网整理和收集了与Java、网络安全、Linux等技术相关的文章,供学习和研究使用。如有侵权,请留言告知,谢谢!

图解设计模式(13):Visitor模式(访问数据结构并处理数据)

设计模式 winrains 1年前 (2019-09-23) 51次浏览

在Visitor模式中,数据结构与处理被分离开来。Visitor是“访问者”的意思。编写一个表示“访问者”的类来访问数据结构中的元素,并把对各元素的处理交给访问者类。这样,当需要增加新的处理时,我们只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可。

1 Visitor模式中的角色

  • Visitor(访问者)

负责对数据结构中每个具体的元素(ConcreteElement角色)声明一个用于访问XXXXX的visit(XXXXX)方法。visit(XXXXX)是用于处理XXXXX的方法,负责实现该方法的是ConcreteVisitor角色。在示例中,对应Visitor类。

  • ConcreteVisitor(具体的访问者)

负责实现Visitor角色所定义的接口。它要实现所有的visit(XXXXX)方法,即实现如何处理每个ConcreteElement角色。在示例中,对应ListVisitor类。

  • Element(元素)

表示Visitor角色的访问对象。它声明了接受访问者的accept方法。accept方法接收到的是参数是Visitor角色。在示例中,对应Element接口。

  • ConcreteElement

负责实现Element角色所定义的接口。在示例中,对应File类和Directory类。

  • ObjectStructure(对象结构)

负责处理Element角色的集合。ConcreteVisitor角色为每个Element角色都准确了处理方法。在示例中,对应Director类(一人分饰两角)。为了让ConcreteVisitor角色可以遍历每个Element角色,在示例中,在Directory类中实现了iterator方法。

2 Visitor模式类图

3 示例程序

在示例程序中,使用Composite模式中用到的那个文件和文件夹的例子作为访问者要访问的数据结构。访问者会访问由文件和文件夹构成的数据结构,然后显示出文件和文件夹的一览。

3.1 类和接口一览表

名字 说明
Visitor 表示访问者的抽象类,它访问文件和文件夹
Element 表示数据结构的接口,它接受访问者的访问
ListVisitor Visitor类的子类,显示文件和文件夹一览
Entry File类和Directory类的父类,它是抽象类,实现了Element接口
File 表示文件的类
Directory 表示文件夹的类
FileTreatementException 表示向文件中add时发生的异常的类
Main 测试程序行为的类

3.2 类图

3.3 示例代码

Visitor类

是表示访问者的抽象类。访问者依赖于它所访问的数据结构(即File类和Directory类)。
visit(File)是用于访问File类的方法,visit(Directory)则是用于访问Directory类的方法。

public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Directory directory);
}

Element接口

是接受访问者访问的接口。
Element接口中声明了accept方法,该方法的参数是访问者Visitor类。

public interface Element {
    public abstract void accept(Visitor v);
}

Entry类

实现了Element接口。add方法仅对Directory类有效,因此在Entry类中,让它简单地报错。同样地,用于获取Iteratoriterator方法也仅对Directory类有效,也让它简单地报错。

import java.util.Iterator;
public abstract class Entry implements Element {
    public abstract String getName();                                   // 获取名字
    public abstract int getSize();                                      // 获取大小
    public Entry add(Entry entry) throws FileTreatmentException {       // 增加目录条目
        throw new FileTreatmentException();
    }
    public Iterator iterator() throws FileTreatmentException {    // 生成Iterator
        throw new FileTreatmentException();
    }
    public String toString() {                                          // 显示字符串
        return getName() + " (" + getSize() + ")";
    }
}

File类

表示文件的类,需要注意该类对accept接口的实现方式。accept方法的参数是Visitor类,然后accept方法的内部处理是”v.visit(this);”,即调用了Visitor类的visit(File)方法。

public class File extends Entry {
    private String name;
    private int size;
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    public String getName() {
        return name;
    }
    public int getSize() {
        return size;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}

Directory类

表示文件夹的类。
iterator方法会返回Iterator,可以使用它遍历文件夹中的所有目录条目。
accept方法与File类中的accept方法相同,Directory类中的accept方法调用了visit(Directory)方法。

import java.util.Iterator;
import java.util.ArrayList;
public class Directory extends Entry {
    private String name;                    // 文件夹名字
    private ArrayList dir = new ArrayList();      // 目录条目集合
    public Directory(String name) {         // 构造函数
        this.name = name;
    }
    public String getName() {               // 获取名字
        return name;
    }
    public int getSize() {                  // 获取大小
        int size = 0;
        Iterator it = dir.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            size += entry.getSize();
        }
        return size;
    }
    public Entry add(Entry entry) {         // 增加目录条目
        dir.add(entry);
        return this;
    }
    public Iterator iterator() {      // 生成Iterator
        return dir.iterator();
    }
    public void accept(Visitor v) {         // 接受访问者的访问
        v.visit(this);
    }
}

ListVisitor类

Visitor类的子类,它的作用是访问数据结构并显示元素一览。
accept方法调用visit方法,visit方法又会调用accept方法,这样就形成了非常复杂的递归调用。通常的递归调用是某个方法调用自身,在Visitor模式中,则是accept方法与visit方法之间相互递归调用。

import java.util.Iterator;
public class ListVisitor extends Visitor {
    private String currentdir = "";                         // 当前访问的文件夹的名字
    public void visit(File file) {                  // 在访问文件时被调用
        System.out.println(currentdir + "/" + file);
    }
    public void visit(Directory directory) {   // 在访问文件夹时被调用
        System.out.println(currentdir + "/" + directory);
        String savedir = currentdir;
        currentdir = currentdir + "/" + directory.getName();
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            entry.accept(this);
        }
        currentdir = savedir;
    }
}

FileTreatmentException类

自定义异常类。

public class FileTreatmentException extends RuntimeException {
    public FileTreatmentException() {
    }
    public FileTreatmentException(String msg) {
        super(msg);
    }
}

Main类

测试程序。
需要注意以下几点:

  • 对于Directory类的实例和File类的实例,我们调用了它们的accept方法;
  • 对于每个Directory类的实例和File类的实例,我们只调用了一次它们的accept方法;
  • 对于ListVisitor的实例,我们调用了它的visit(Directory)visit(File)方法;
  • 处理visit(Directory)visit(File)的是同一个ListVisitor的实例。
public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("Making root entries...");
            Directory rootdir = new Directory("root");
            Directory bindir = new Directory("bin");
            Directory tmpdir = new Directory("tmp");
            Directory usrdir = new Directory("usr");
            rootdir.add(bindir);
            rootdir.add(tmpdir);
            rootdir.add(usrdir);
            bindir.add(new File("vi", 10000));
            bindir.add(new File("latex", 20000));
            rootdir.accept(new ListVisitor());
            System.out.println("");
            System.out.println("Making user entries...");
            Directory yuki = new Directory("yuki");
            Directory hanako = new Directory("hanako");
            Directory tomura = new Directory("tomura");
            usrdir.add(yuki);
            usrdir.add(hanako);
            usrdir.add(tomura);
            yuki.add(new File("diary.html", 100));
            yuki.add(new File("Composite.java", 200));
            hanako.add(new File("memo.tex", 300));
            tomura.add(new File("game.doc", 400));
            tomura.add(new File("junk.mail", 500));
            rootdir.accept(new ListVisitor());
        } catch (FileTreatmentException e) {
            e.printStackTrace();
        }
    }
}

运行结果

Making root entries...
/root (30000)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (0)
Making user entries...
/root (31500)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (1500)
/root/usr/yuki (300)
/root/usr/yuki/diary.html (100)
/root/usr/yuki/Composite.java (200)
/root/usr/hanako (300)
/root/usr/hanako/memo.tex (300)
/root/usr/tomura (900)
/root/usr/tomura/game.doc (400)
/root/usr/tomura/junk.mail (500)

4 总结

4.1 双重分发

整理一下Visitor模式中方法的调用关系:
accept方法的调用方式如下:
element.accept(visitor);
visit方法的调用方式如下:
visitor.visit(element);
对比一下这两个方法会发现,它们是相反的关系。element接受visitor,而visitor又访问element
在Visitor模式中,ConcreteElement和ConcreteVisitor这两个角色共同决定了实际进行的处理。这种消息分发的方式一般被称为双重分发。

4.2 为什么设计如此复杂

Visitor模式的目的是将处理从数据结构中分离出来。在示例中,创建了ListVisitor类作为显示文件夹内容的ConcreteVisitor角色。此外,还可以编写进行其它处理的ConcreteVisitor角色。通常,ConcreteVisitor角色的开发可以独立于File类和Directory类。Visitor模式提高了File类和Directory类作为组件的独立性。如果将进行处理的方法定义在File类和Directory类中,当每次要扩展功能,增加新的处理时,就不得不去修改File类和Directory类。

4.3 开闭原则—对扩展开放,对修改关闭

在设计类时,若无特殊理由,必须要考虑到将来可能会扩展类。绝不能毫无理由地禁止扩展类。这就是“对扩展开放”的意思。
但是,如果在每次扩展类时都需要修改现有的类就太麻烦了。所以我们需要在不用修改现有类的前提下能够扩展类,这就是“对修改关闭”的意思。
对扩展开放、对修改关闭的类具有高可复用性,可作为组件复用。设计模式和面向对象的目的正是为我们提供一种结构,可以帮助我们设计出这样的类。

4.4 易于增加ConcreteVisitor角色

使用Visitor模式可以很容易地增加ConcreteVisitor角色。因为具体的处理被交给ConcreteVisitor角色负责,完全不用修改ConcreteElement角色。

4.5 难于增加ConcreteElement角色

虽然使用Visitor模式可以很容易地增加ConcreteVisitor角色,但却很难应对ConcreteElement角色的增加。
例如,假设现成需要在示例中增加Entry类的子类Device类,这时需要在Visitor类中声明一个visitor(Device)方法,并且所有的Visitor类的子类中都实现这个方法。

4.6 Visitor工作所需的条件

在示例中,visit(Directory)方法需要调用每个目录条目的accept方法,为此,Directory类必须提供用于获取每个目录条目的iterator方法。
访问者只有从数据结构中获取了足够多的信息后才能工作。如果无法获取这些信息,它就无法工作。这样做的缺点是,如果公开了不应当被公开的信息,将来对数据结构的改良会变得非常困难。

摘自《图解设计模式》


版权声明:文末如注明作者和来源,则表示本文系转载,版权为原作者所有 | 本文如有侵权,请及时联系,承诺在收到消息后第一时间删除 | 如转载本文,请注明原文链接。
喜欢 (1)