设计模仿访客没有其缺点

时间:2012-01-12 14:17:19

标签: java visitor-pattern double-dispatch

我正在寻找一种干净的设计来模仿访客功能而没有它带来的许多缺点。 在Java中,传统的实现(如GoF中所描述的)采用双重调度来摆脱if-elses。为了解决这个问题,我看到一些实现使用反射来避免对“可访问”类进行修改,但是这些实现在查找方法名时依赖于硬编码字符串。虽然非常有用,但我仍然认为它们不是干净的设计。

是否可以使用数据结构和/或良好的OO设计来模拟相同的想法?它不一定是一个模式,只是我在寻找解决类似问题的例子(例如:使用Map<Class<T>,SomeFunctionObject>)。


更新类似的内容:

    public abstract class BaseVisitor<T> {

        private final TypesafeHeterogeneusMap map;

        protected BaseVisitor(){
            map = inflateFunctions();
        }   

        public <E extends T> void  process(E element){
            if(element == null){
                throw new NullPointerException();
            }
            boolean processed = false;

            @SuppressWarnings("unchecked")
            Class<? super T> sc = (Class<? super T>) element.getClass();

            while(true){            
                if(sc != null){
                    FunctionObject<? super T> fo2 = map.get(sc);
                    if(fo2 != null){
                        fo2.process(element);
                        processed = true;
                        break;
                    }
                    sc = sc.getSuperclass();
                } else {
                    break;
                }
            }

            if(!processed) System.out.println("Unknown type: " + element.getClass().getName());     
        }

        abstract TypesafeHeterogeneusMap inflateFunctions();
    }

我认为实际上是模板模式和命令模式的混合。请随意发表有关如何增强它的建议。

3 个答案:

答案 0 :(得分:3)

您可以让所有的Visitor实现扩展一个基类,它为每种类型的Visitable提供默认实现:

public interface AnimalVisitor {
    void visitHorse(Horse horse);
    void visitDog(Dog dog);
}

public class BaseAnimalVisitor implements AnimalVisitor {
    public void visitHorse(Horse horse) {
        // do nothing by default
    }
    public void visitDog(Dog dog) {
        // do nothing by default
    }
}

然后,当引入新类Cat时,将visitCat(Cat cat)方法添加到接口和基类,并且所有访问者保持不变并仍然编译。如果他们不想忽略猫,那么你覆盖visitCat方法。

答案 1 :(得分:2)

虽然这不是您正在寻找的答案:考虑使用比Java更高级,更简洁的语言。你会发现像访客模式这样的东西似乎开始变得无关紧要了。当然,如果你想在一个地方定义遍历数据结构的逻辑,并在其他地方定义如何处理数据结构的元素(基于它们的类型),并使混合和匹配遍历成为可能/处理策略,你可以做到这一点。但是你可以使用少量简单的代码来做到这一点,你不会想到称之为“模式”。

我来自C / Java编程背景,并在几年前开始学习各种动态语言。想知道你能用几行代码做多少就是令人兴奋的事。

例如,如果我要在Ruby中模拟访问者模式:

module Enumerable
  def accept_visitor(visitor)
    each do |elem|
      method = "visit#{elem.class}".to_sym
      elem.send(method,elem) if elem.respond_to? method
    end
  end
end

解释:在Ruby中,Enumerable表示可以迭代的任何内容。在这8行代码中,我创建了每个类型的对象,可以通过接受访问者进行迭代。无论我计划有5个,10个还是100个不同的班级接受访客,这8行都是必需的。

以下是访问者样本:

class CatCounter
  attr_reader :count
  def initialize; @count  = 0; end
  def visitCat;   @count += 1; end
end

请注意,访问者不必为所有定义不同类型的Visitables的方法。每个访问者只需要为其感兴趣的可访问类型定义方法;它可以忽略其余的。 (这意味着如果添加新的Visitable类型,则无需修改大量现有代码。)任何访问者可以与接受访问者的任何对象进行互操作

就在这几行代码中,您提到的访问者模式中遇到的所有问题都已被克服。

别误会我的意思; Java是一些很好的语言。但是你需要为工作选择合适的工具。您为克服工具的限制而奋斗的事实可能表明,在这种情况下,需要使用不同的工具。

答案 2 :(得分:0)

@MisterSmith,因为你必须使用Java,并且可能你有充分的理由使用Visitor,我将提出另一种可能的解决方案。

让我们将观点与访客通常的实施方式区分开来,并回到人们首先使用访客的原因。虽然我在其他答案中已经提到过,但访问者的观点是能够混合和匹配遍历和处理逻辑。

“遍历逻辑”可以表示遍历不同类型的数据结构或以不同顺序遍历相同数据结构的逻辑。或者它甚至可以包括遍历策略,它将某些过滤器应用于返回的元素等等。

访问者隐含的意思是我们应用于每个元素的处理将取决于其类。如果我们对每个元素的操作不依赖于它的类,则没有理由使用Visitor。除非我们想对元素类进行“切换”,否则我们需要使用虚方法调用来执行此操作(这就是通常的Java实现使用双重调度的原因)。

我建议我们可以将访客模式拆分为 3 而不是2部分:

  1. 实现特定遍历的Iterator对象

  2. 实现“基于其类决定如何处理元素”策略的对象(通常需要双重调度的部分)。使用反射,我们可以创建一个通用类来执行此操作。一个简单的实现将使用Map,或者您可以创建一些动态生成字节码的东西(我忘记了Java中的平台方法,它允许您将原始字节码作为新类加载,但有一个)。要么!或者,您可以使用动态的JVM托管语言(如JRuby或Clojure)编写#2,编译为字节码,并使用生成的.class文件。 (这个文件可能会使用invokedynamic字节码,据我所知,它不能从Java访问 - Java编译器永远不会发出它。如果这已经改变,请编辑这篇文章。)

  3. 访客本身。在此实现中,Visitors不必从公共超类中继承子类,也不必为他们不感兴趣的元素实现方法。

  4. 将遍历保存在通用迭代器中允许您使用它进行其他操作(不仅仅是接受访问者)。

    有3种方法可以绑在一起;我认为#2将包装#3(将其作为构造函数参数)。 #2将提供一个公共方法,它将Iterator作为参数,并将Visitor应用于它。

    有趣的部分是#2。我稍后可以编辑这篇文章来添加一个示例实现;现在我还有其他一些事情要做。如果其他人想出了一个实现,请在此处添加。