Java实现在访问者模式中使用反射

时间:2022-04-14 22:13:06

集合类型在面向对象编程中很常用,这也带来一些代码相关的问题。比如,“怎么操作集合中不同类型的对象?”

一种做法就是遍历集合中的每个元素,然后根据它的类型而做具体的操作。这会很复杂,尤其当你不知道集合中元素的类型时。如果y要打印集合中的元素,可以写一个这样的方法:

public void messyPrintCollection(Collection collection) {
    Iterator iterator = collection.iterator()
    while (iterator.hasNext())
        System.out.println(iterator.next().toString())
}

看起来很简单。仅仅调用了Object.toString()方法并打印出了对象,对吧?但如果你的集合是一个包含hashtable的vector呢?那会变得更复杂。你必须检查集合返回对象的类型:

public void messyPrintCollection(Collection collection) {
    Iterator iterator = collection.iterator()
    while (iterator.hasNext()) {
        Object o = iterator.next();
        if (o instanceof Collection)
            messyPrintCollection((Collection)o);
        else
            System.out.println(o.toString());
        }
}

好了,现在可以处理内嵌的集合对象,但其他对象返回的字符串不是你想要的呢?假如你想在字符串对象加上引号,想在Float对象后加一个f,你该怎么做?代码会变得更加复杂:

public void messyPrintCollection(Collection collection) {
    Iterator iterator = collection.iterator()
    while (iterator.hasNext()) {
        Object o = iterator.next();
        if (o instanceof Collection)
            messyPrintCollection((Collection)o);
        else if (o instanceof String)
            System.out.println("'"+o.toString()+"'");
        else if (o instanceof Float)
            System.out.println(o.toString()+"f");
        else
            System.out.println(o.toString());
    }
}

代码很快就变杂乱了。你不想让代码中包含一大堆的if-else语句!怎么避免呢?访问者模式可以帮助你。

为实现访问者模式,你需要创建一个Visitor接口,为被访问的集合对象创建一个Visitable接口。接下来需要创建具体的类来实现Visitor和Visitable接口。这两个接口大致如下:

public interface Visitor
{
    public void visitCollection(Collection collection);
    public void visitString(String string);
    public void visitFloat(Float float);
}
public interface Visitable
{
    public void accept(Visitor visitor);
}

对于一个具体的String类,可以这么实现:

public class VisitableString implements Visitable
{
    private String value;
    public VisitableString(String string) {
        value = string;
    }
    public void accept(Visitor visitor) {
        visitor.visitString(this);
    }
}

在accept方法中,根据不同的类型,调用visitor中对应的方法:

visitor.visitString(this)

具体Visitor的实现方式如下:

public class PrintVisitor implements Visitor
{
    public void visitCollection(Collection collection) {
        Iterator iterator = collection.iterator();
        while (iterator.hasNext()) {
            Object o = iterator.next();
            if (o instanceof Visitable)
                ((Visitable)o).accept(this);
    }
    public void visitString(String string) {
        System.out.println("'"+string+"'");
    }
    public void visitFloat(Float float) {
        System.out.println(float.toString()+"f");
    }
}

到时候,只要实现了VisitableFloat类和VisitableCollection类并调用合适的visitor方法,你就可以去掉包含一堆if-else结构的messyPrintCollection方法,采用一种十分清爽的方式实现了同样的功能。visitCollection()方法调用了Visitable.accept(this),而accept()方法又反过来调用了visitor中正确的方法。这就是双分派:Visitor调用了一个Visitable类中的方法,这个方法又反过来调用了Visitor类中的方法。

尽管实现visitor后,if-else语句不见了,但还是引入了很多附加的代码。你不得不将原始的对象——String和Float,打包到一个实现Visitable接口的类中。虽然很烦人,但这一般来说不是个问题。因为你可以限制被访问集合只能包含Visitable对象。

然而,这还有很多附加的工作要做。更坏的是,当你想增加一个新的Visitable类型时怎么办,比如VisitableInteger?这是访问者模式的一个主要缺点。如果你想增加一个新的Visitable类型,你不得不改变Visitor接口以及每个实现Visitor接口方法的类。你可以不把Visitor设计为接口,取而代之,可以把Visitor设计为一个带有空操作的抽象基类。这与Java GUI中的Adapter类很相似。这么做的问题是你会用尽单次继承,而常见的情形是你还想用继承实现其他功能,比如继承StringWriter类。这同样只能成功访问实现Visitable接口的对象。

幸运的是,Java可以让你的访问者模式更灵活,你可以按你的意愿增加Visitable对象。怎么实现呢?答案是使用反射。使用反射的ReflectiveVisitor接口只需要一个方法:

public interface ReflectiveVisitor {
    public void visit(Object o);
}

好了,上面很简单。Visitable接口先不动,待会我会说。现在,我使用反射实现PrintVisitor类。

public class PrintVisitor implements ReflectiveVisitor {
    public void visitCollection(Collection collection)
    { ... same as above ... }
    public void visitString(String string)
    { ... same as above ... }
    public void visitFloat(Float float)
    { ... same as above ... }
    public void default(Object o)
    {
        System.out.println(o.toString());
    }
    public void visit(Object o) {
        // Class.getName() returns package information as well.
        // This strips off the package information giving us
        // just the class name
        String methodName = o.getClass().getName();
        methodName = "visit"+
                    methodName.substring(methodName.lastIndexOf('.')+1);
        // Now we try to invoke the method visit<methodName>
        try {
            // Get the method visitFoo(Foo foo)
            Method m = getClass().getMethod(methodName,
                new Class[] { o.getClass() });
            // Try to invoke visitFoo(Foo foo)
            m.invoke(this, new Object[] { o });
        } catch (NoSuchMethodException e) {
            // No method, so do the default implementation
            default(o);
        }
    }
}

现在你无需使用Visitable包装类(包装了原始类型String、Float)。你可以直接访问visit(),它会调用正确的方法。visit()的一个优点是它会分派它认为合适的方法。这不一定使用反射,可以使用完全不同的一种机制。

在新的PrintVisitor类中,有对应于Collections、String和Float的操作方法;对于不能处理的类型,可以通过catch语句捕捉。对于不能处理的类型,可以通过扩展visit()方法来尝试处理它们的所有超类。首先,增加一个新的方法getMethod(Class c),返回值是一个可被触发的方法。它会搜索Class c的所有父类和接口,以找到一个匹配方法。

protected Method getMethod(Class c) {
    Class newc = c;
    Method m = null;
    // Try the superclasses
    while (m == null && newc != Object.class) {
        String method = newc.getName();
        method = "visit" + method.substring(method.lastIndexOf('.') + 1);
        try {
            m = getClass().getMethod(method, new Class[] {newc});
        } catch (NoSuchMethodException e) {
            newc = newc.getSuperclass();
        }
    }
    // Try the interfaces.  If necessary, you
    // can sort them first to define 'visitable' interface wins
    // in case an object implements more than one.
    if (newc == Object.class) {
        Class[] interfaces = c.getInterfaces();
        for (int i = 0; i < interfaces.length; i++) {
            String method = interfaces[i].getName();
            method = "visit" + method.substring(method.lastIndexOf('.') + 1);
            try {
                m = getClass().getMethod(method, new Class[] {interfaces[i]});
            } catch (NoSuchMethodException e) {}
        }
    }
    if (m == null) {
        try {
            m = thisclass.getMethod("visitObject", new Class[] {Object.class});
        } catch (Exception e) {
            // Can't happen
        }
    }
    return m;
}

这看上去很复杂,实际上并不。大致来说,首先根据传入的class名称搜索可用方法;如果没找到,就尝试从父类搜索;如果还没找到,就从接口中尝试。最后,(仍没找到)可以使用visitObject()作为默认方法。

由于大家对传统的访问者模式比较熟悉,这里沿用了之前方法命名的惯例。但是,有些人可能注意到,把所有的方法都命名为“visit”并通过参数类型不同来区分,这样更高效。然而,如果你这么做,你必须把visit(Object o)方法的名称改为其他,比如dispatch(Object o)。否则,(当没有对应处理方法时),你无法退回到默认的处理方法,并且当你调用visit(Object o)方法时,为了确保正确的方法调用,你必须将参数强制转化为Object。

为了利用getMethod()方法,现在需要修改一下visit()方法。

public void visit(Object object) {
    try {
        Method method = getMethod(getClass(), object.getClass());
        method.invoke(this, new Object[] {object});
    } catch (Exception e) { }
}

现在,visitor类更加强大了——可以传入任意的对象并且有对应的处理方法。另外,有一个默认处理方法,visitObject(Object o),的好处就是就可以捕捉到任何没有明确说明的类型。再稍微修改下,你甚至可以添加一个visitNull()方法。

我仍保留Visitable接口是有原因的。传统访问者模式的另一个好处是它可以通过Visitable对象控制对象结构的遍历顺序。举例来说,假如有一个实现了Visitable接口的类TreeNode,它在accept()方法中遍历自己的左右节点。

public void accept(Visitor visitor) {
    visitor.visitTreeNode(this);
    visitor.visitTreeNode(leftsubtree);
    visitor.visitTreeNode(rightsubtree);
}

这样,只要修改下Visitor类,就可以通过Visitable类控制遍历:

public void visit(Object object) throws Exception
{
    Method method = getMethod(getClass(), object.getClass());
    method.invoke(this, new Object[] {object});
    if (object instanceof Visitable)
    {
        callAccept((Visitable) object);
    }
}
public void callAccept(Visitable visitable) {
    visitable.accept(this);
}

如果你实现了Visitable对象的结构,你可以保持callAccept()不变,就可以使用Visitable控制的对象遍历。如果你想在visitor中遍历对象结构,你只需重写allAccept()方法,让它什么都不做。

当使用几个不同的visitor去操作同一个对象集合时,访问者模式的力量就会展现出来。比如,当前有一个解释器、中序遍历器、后续遍历器、XML编写器以及SQL编写器,它们可以处理同一个对象集合。我可以轻松地为这个集合再写一个先序遍历器或者一个SOAP编写器。另外,它们可以很好地兼容它们不识别的类型,或者我愿意的话可以让它们抛出异常。

总结

使用Java反射,可以使访问者模式提供一种更加强大的方式操作对象结构,可以按照需求灵活地增加新的Visitable类型。我希望在你的编程之旅中可以使用访问者模式。

Jeremy Blosser有5年的Java编程经验,他在很多软件公司工作过。他现在在一家创业型公司Software Instruments供职。你可以访问Jeremy的网站http://www.blosser.org

了解更多

Java实现在访问者模式中使用反射的更多相关文章

  1. 折腾Java设计模式之访问者模式

    博客原文地址:折腾Java设计模式之访问者模式 访问者模式 Represent an operation to be performed on the elements of an object st ...

  2. JAVA设计模式之访问者模式

    在阎宏博士的<JAVA与模式>一书中开头是这样描述访问者(Visitor)模式的: 访问者模式是对象的行为模式.访问者模式的目的是封装一些施加于某种数据结构元素之上的操作.一旦这些操作需要 ...

  3. 15&period;java设计模式之访问者模式

    基本需求: 电脑需要键盘鼠标等固定的组件组成 现在分为个人,组织等去买电脑,而同一种组件对不同的人(访问者)做出不同的折扣,从而电脑的价格也不一样 传统的解决方法:在组件内部进行判断访问人的类型,从而 ...

  4. 由电脑专卖系统引发的Java设计模式:访问者模式

    目录 定义 意图 解决问题 何时使用 优缺点 结构 电脑专卖系统 定义 访问者模式是对象的行为型模式,它的目的是封装一些施加于某些数据结构元素之上的操作,一旦这些操作需要修改的话,接收这个操作的数据结 ...

  5. 13、Visitor 访问者模式 访问数据结构并处理数据 行为型设计模式

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

  6. Java设计模式:桥接模式

    问题提出 生活中有很多事物集合,设为A1,A2......Am ,而每个事物都有功能F1,F2....Fn. 例如邮局的发送业务.简单模拟有两类事物:信件和包裹,均有平邮和挂号邮寄功能.程序设计中如何 ...

  7. 设计模式 -- 访问者模式&lpar;Visitor&rpar;

    写在前面的话:读书破万卷,编码如有神--------------------------------------------------------------------主要内容包括: 初识访问者模 ...

  8. 浅谈设计模式-visitor访问者模式

    先看一个和visitor无关的案例.假设你现在有一个书架,这个书架有两种操作,1添加书籍2阅读每一本书籍的简介. //书架public class Bookcase { List<Book&gt ...

  9. 访问者模式&lpar;Visitor Pattern&rpar;——操作复杂对象结构

    模式概述 在软件开发中,可能会遇到操作复杂对象结构的场景,在该对象结构中存储了多个不同类型的对象信息,而且对同一对象结构中的元素的操作方式并不唯一,可能需要提供多种不同的处理方式,还有可能增加新的处理 ...

随机推荐

  1. &period;NET中的逆变协变

    MSDN上的说法: 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型----------(注 ...

  2. DotNetBar for Windows Forms 12&period;5&period;0&period;2&lowbar;冰河之刃重打包版原创发布-带官方示例程序版

    关于 DotNetBar for Windows Forms 12.5.0.2_冰河之刃重打包版 --------------------11.8.0.8_冰河之刃重打包版-------------- ...

  3. OpenWRT连接OPENVPN的教程

    这是相当基本没有任何web界面,只是几个命令如何运行OpenWRT的 OpenVPN的例子. OpenWRT的设置更复杂,所以这个教程仅供爱好者和经验的用户使用参考. 本教程假定您有OpenWRT的安 ...

  4. php面试题之四——Linux部分(高级部分)

    四.Linux部分 1.请解释下列10个shell命令的用途(新浪网技术部) top.ps.mv.find.df.cat.chmod.chgrp.grep.wc top:该命令提供了实时对系统处理器状 ...

  5. 对于我的linux常用命令的说明

    我所列出的linux常用命令中的选项并不是全部的选项,是一些我们经常用到的linux命令及选项

  6. linux下gcc编译的参数详细说明

    参考网址:1 http://hi.baidu.com/zengzhaonong/item/f1f9383565fa5c302e0f8125 gcc使用方法 汇总 2 http://s99f.blog. ...

  7. html5 手机APP计算高度问题

    安卓手机型号比较多,会出现bottom:0 找不到底部的问题,所以需要计算手机可视区域高度,这样便于使用百分比适配 (function(window,undefined){ /** * js_heig ...

  8. Binary Tree Level Order Traversal&comma;Binary Tree Level Order Traversal II

    Binary Tree Level Order Traversal Total Accepted: 79463 Total Submissions: 259292 Difficulty: Easy G ...

  9. &lbrack;Math&rsqb;Sqrt&lpar;x&rpar;

    Total Accepted: 75767 Total Submissions: 314003 Difficulty: Medium Implement int sqrt(int x). Comput ...

  10. 使用Dotfuscator加密混淆程序以及如何脱壳反编译

    混淆演示 首先介绍如何使用Dotfuscator对.net程序加密码混淆/加壳 C#或vb.net编写的应用程序或DLL. 这里随便创建了一个C#的命令行控制台程序.程序很简单,对当前的时间进行了AE ...