首页 > 技术文章 > 《图解设计模式》读书笔记6-1 VISITOR模式

qianbixin 2019-06-02 18:16 原文

1. Visitor模式简介

​ Visitor是访问者的意思。

​ 一个数据结构包含多种数据元素,这些数据元素方便存储,却不一定方便使用。因此我们有必要架起一座桥梁,即对数据元素进行处理,方便别人使用。问题来了:数据的处理是放在数据结构类里面还是再造一个类专门用来处理?

​ 如果一个数据结构需要多种处理方式,那每增加一种就要修改数据结构的类,显然不合适。访问者模式就是解决这个问题来的。在这个模式中,数据结构和处理被分离开。我们编写一个访问者,即专门处理数据元素的类来处理数据元素,一旦有新的处理方式,就编写新的访问者类去访问数据结构即可。

2. 示例

接下来的例子以之前的Composite模式为基础编写。为File和Directory类添加一个accept方法,这个方法接收Visitor类,即访问者类。通过这个访问者我们能获得File和Directory类的经过处理的数据展示。

2.1 类图

2.2 代码

ListVisitor里面的visit(Directory directory)使用了递归,值得好好体会一下。

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

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

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

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 {
        throw new FileTreatmentException();
    }
    public String toString() {                                          // 显示字符串
        return getName() + " (" + getSize() + ")";
    }
}

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

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

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 xiaoming = new Directory("小明");
            Directory xiaohong = new Directory("小红");
            Directory xiaohua = new Directory("小花");
            usrdir.add(xiaoming);
            usrdir.add(xiaohong);
            usrdir.add(xiaohua);
            xiaoming.add(new File("diary.html", 100));
            xiaoming.add(new File("Composite.java", 200));
            xiaohong.add(new File("memo.tex", 300));
            xiaohua.add(new File("game.doc", 400));
            xiaohua.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/小明 (300)
/root/usr/小明/diary.html (100)
/root/usr/小明/Composite.java (200)
/root/usr/小红 (300)
/root/usr/小红/memo.tex (300)
/root/usr/小花 (900)
/root/usr/小花/game.doc (400)
/root/usr/小花/junk.mail (500)

3. 模式的角色和类图

  • Visitor(访问者): 抽象的访问者类,里面声明了供Element调用的访问方法visit。本例中由Visitor类扮演此角色。
  • ConcreteVisitor(具体的访问者):具体的访问者类,里面实现了供Element调用的访问方法visit,本例中由ListVisitor类扮演此角色。
  • Element(元素):表示Visitor访问的对象,声明了接收访问者的accept方法,参数是Visitor角色,本例中由Element接口扮演此角色。
  • ConcreteElement(具体的元素):实现Element定义的接口,本例中由File类和Directory类扮演此角色。
  • ObjectStructure(对象结构):负责处理Element角色的集合。ConcreteVisitor角色为每个Element角色都准备了处理方法,在本例中,由Directory类扮演此角色。为了让ConcreteVisitor可以遍历处理每个Element角色,在示例程序中,我们在Directory类中实现了iterator方法。

4. 思路拓展

4.1 双重分发

在代码中,元素接受访问者:element.accept(visitor),访问者访问元素visitor.visit(element),对比这两个方法,他们是相反的关系,这种消息分发的方式被称为双重分发。

4.2 开闭原则

  • 对扩展开放

  • 对修改关闭

如果要增加功能,要在不修改现有代码的前提下进行扩展。Visitor模式中,如果要扩展数据结构的访问方法,不需要修改数据结构的类, 只需要增加一个访问者类即可。体现了开闭原则。

4.3 难以增加ConcreteElement角色

​ Visitor模式很容易增加ConcreteVisitor角色,却很难应对ConcreteElement角色的增加。假设我们要增加Entry类的子类Device类,这个类和File类、Directory类是兄弟关系。我们就需要在Visitor类中声明一个visit(Device)方法,所有Visitor类的子类都要实现这个方法。

4.4 Visitor工作所需的条件

​ Visitor需要从数据结构中获取到足够多的信息才能够工作。如果数据结构向Visitor公开了不该公开的信息,将来对数据结构的改良就会变得非常困难。

推荐阅读