1

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

是否可以使用数据结构和/或良好的 OO 设计来模拟相同的想法?它不一定是模式,只是我正在寻找解决类似问题的示例(例如:使用 a 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();
    }

我认为实际上是模板模式和命令模式的混合。随时发表您对如何增强它的建议。

4

3 回答 3

3

你可以让你的所有访问者实现扩展一个基类,它为每种类型的访问提供了一个默认的实现:

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方法。

于 2012-01-12T14:36:29.237 回答
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

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

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

不要误会我的意思;Java 对于某些事情来说是一门很棒的语言。但是您需要为工作选择正确的工具。您正在努力克服工具的局限性这一事实可能表明,在这种情况下,需要使用不同的工具。

于 2012-01-23T19:45:35.013 回答
0

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

让我们将我们的思想与通常实施访问者的方式分开,并回到人们首先使用访问者的原因。尽管我在其他答案中已经提到过,但访问者的目的是使混合匹配遍历和处理逻辑成为可能。

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

Visitor 中隐含的想法是,我们应用于每个元素的处理将取决于其类。如果我们对每个元素所做的事情不依赖于它的类,那么就没有理由使用 Visitor。除非我们想对元素类进行“切换”,否则我们需要使用虚方法调用来做到这一点(这就是通常的 Java 实现使用双重分派的原因)。

我建议我们可以将访问者模式分成3部分而不是 2 部分:

  1. 实现某种遍历的 Iterator 对象

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

  3. 游客自己。在这个实现中,访问者不必从一个公共超类继承,也不必为他们不感兴趣的元素实现方法。

将遍历保持在通用迭代器中允许您用它做其他事情(不仅仅是接受访问者)。

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

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

于 2012-01-24T10:04:22.193 回答