【问题标题】:Confused about the Visitor Design Pattern对访客设计模式感到困惑
【发布时间】:2021-06-03 04:54:39
【问题描述】:

所以,我只是在阅读访问者模式,发现访问者和元素之间的来回很奇怪!

基本上我们称之为元素,我们将访问者传递给它,然后元素将自身传递给访问者。然后访问者操作元素。什么?为什么?感觉太没必要了。我称之为“来回疯狂”。

因此,当需要在所有元素上执行相同的操作时,访问者的目的是将元素与其操作分离。这是为了防止我们需要用新的动作来扩展我们的元素,我们不想进入所有这些类并修改已经稳定的代码。所以我们在这里遵循 Open/Closed 原则。

为什么会有这么多来回,如果我们没有这些,我们会失去什么?

例如,我编写的这段代码牢记了这一目的,但跳过了访问者模式的疯狂交互。基本上我有会跳和吃的动物。我想将这些动作与对象分离,因此我将这些动作移至访客。进食和跳跃会增加动物的健康(我知道,这是一个非常愚蠢的例子……)

public interface AnimalAction { // Abstract Visitor
    public void visit(Dog dog);
    public void visit(Cat cat);
}

public class EatVisitor implements AnimalAction { // ConcreteVisitor
    @Override
    public void visit(Dog dog) {
        // Eating increases the dog health by 100
        dog.increaseHealth(100);
    }

    @Override
    public void visit(Cat cat) {
        // Eating increases the cat health by 50
        cat.increaseHealth(50);
    }
}

public class JumpVisitor implements AnimalAction { // ConcreteVisitor
    public void visit(Dog dog) {
        // Jumping increases the dog health by 10
        dog.increaseHealth(10);
    }

    public void visit(Cat cat) {
        // Jumping increases the cat health by 20
        cat.increaseHealth(20);
    }
}

public class Cat { // ConcreteElement
    private int health;

    public Cat() {
        this.health = 50;
    }

    public void increaseHealth(int healthIncrement) {
        this.health += healthIncrement;
    }

    public int getHealth() {
        return health;
    }
}

public class Dog { // ConcreteElement

    private int health;

    public Dog() {
        this.health = 10;
    }

    public void increaseHealth(int healthIncrement) {
        this.health += healthIncrement;
    }

    public int getHealth() {
        return health;
    }
}

public class Main {

    public static void main(String[] args) {
        AnimalAction jumpAction = new JumpVisitor();
        AnimalAction eatAction = new EatVisitor();

        Dog dog = new Dog();
        Cat cat = new Cat();

        jumpAction.visit(dog); // NOTE HERE. NOT DOING THE BACK AND FORTH MADNESS.
        eatAction.visit(dog);
        System.out.println(dog.getHealth());

        jumpAction.visit(cat);
        eatAction.visit(cat);
        System.out.println(cat.getHealth());
    }
}

【问题讨论】:

  • 恕我直言,在与访问者进行推理时,我通常使用树形模式,其中您有不同类型的 Nodes(例如,考虑一些节点代表运算符的 AST,而其他操作数等则代表 @987654325 @ 例如Equals(Plus(Literal(1), Literal(1)), Literal(2)) 显然Equals/Plus 等是通用的,因为表达式不需要是简单的文字,现在想想你将如何编写一个评估这个表达式的访问者......然后写一些代码解析表达式的文本并对其进行评估,现在您在编译时有 0 个已知的具体类
  • @GACy20 你是对的。这段代码有效的唯一原因是因为我已经知道狗是狗,猫是猫。感谢这个例子,它给访问者更多的意义。
  • 使用dog 的代码和使用cat 的代码看起来相同,所以如果您尝试将它们放入列表并使用会发生什么一个循环?
  • 我认为 Shvets 很好地合理化了访问者模式;当您需要 扩展现有类 以实现一些常见行为时,您可以使用它 - 因此您需要使用“双重调度” - sourcemaking.com/design_patterns/visitor 当您不需要时有这个约束要处理,通常有更简单的方法来实现相同的目标。
  • 另见我的回答here,它可能会给你带来一些进一步的见解。

标签: java design-patterns visitor-pattern


【解决方案1】:

OP 中的代码类似于众所周知的访问者设计模式的变体,称为 内部访问者(参见例如 Extensibility for the Masses。Practical Extensibility with Object Algebras > Bruno C. d. S. Oliveira 和 William R. Cook)。然而,这种变体使用泛型和返回值(而不是 void)来解决访问者模式解决的一些问题。

这是什么问题,为什么 OP 变异可能不足?

访问者模式解决的主要问题是当您有异构对象时需要对其进行处理。正如四人帮Design Patterns 的作者)所说,当

“一个对象结构包含许多具有不同接口的对象类,您希望对这些对象执行依赖于它们的具体类的操作。”

这句话缺少的是,虽然您希望“对这些依赖于其具体类的对象执行操作”,但您希望将这些具体类视为具有单一多态类型。

一个时期的例子

使用 animal 域很少是说明性的(我稍后会谈到),所以这里有另一个更现实的例子。示例在 C# 中 - 我希望它们对您仍然有用。

假设您正在开发一个在线餐厅预订系统。作为该系统的一部分,您需要能够向用户显示日历。此日历可以显示给定日期的剩余座位数,或列出当天的所有预订。

有时,您希望显示一天,但在其他时候,您希望将整个月份显示为单个日历对象。投入一整年以获得良好的衡量标准。这意味着您有三个时期:年。每个都有不同的接口:

public Year(int year)

public Month(int year, int month)

public Day(int year, int month, int day)

为简洁起见,这些只是三个独立类的构造函数。许多人可能只是将其建模为具有可为空字段的单个类,但这会迫使您处理空字段、枚举或其他类型的脏东西。

上述三个类具有不同的结构,因为它们包含不同的数据,但您希望将它们视为一个概念 - 句号

为此,定义一个IPeriod 接口:

internal interface IPeriod
{
    T Accept<T>(IPeriodVisitor<T> visitor);
}

并使每个类都实现接口。这里是Month

internal sealed class Month : IPeriod
{
    private readonly int year;
    private readonly int month;

    public Month(int year, int month)
    {
        this.year = year;
        this.month = month;
    }

    public T Accept<T>(IPeriodVisitor<T> visitor)
    {
        return visitor.VisitMonth(year, month);
    }
}

这使您可以将三个异构类视为单一类型,并在该单一类型上定义操作,而无需更改接口。

例如,这里是一个计算上一个周期的实现:

private class PreviousPeriodVisitor : IPeriodVisitor<IPeriod>
{
    public IPeriod VisitYear(int year)
    {
        var date = new DateTime(year, 1, 1);
        var previous = date.AddYears(-1);
        return Period.Year(previous.Year);
    }

    public IPeriod VisitMonth(int year, int month)
    {
        var date = new DateTime(year, month, 1);
        var previous = date.AddMonths(-1);
        return Period.Month(previous.Year, previous.Month);
    }

    public IPeriod VisitDay(int year, int month, int day)
    {
        var date = new DateTime(year, month, day);
        var previous = date.AddDays(-1);
        return Period.Day(previous.Year, previous.Month, previous.Day);
    }
}

如果您有Day,您将获得之前的Day,但如果您有Month,您将获得之前的Month,依此类推。

您可以在this article 中看到PreviousPeriodVisitor 类和其他正在使用的访问者,但以下是使用它们的几行代码:

var previous = period.Accept(new PreviousPeriodVisitor());
var next = period.Accept(new NextPeriodVisitor());

dto.Links = new[]
{
    url.LinkToPeriod(previous, "previous"),
    url.LinkToPeriod(next, "next")
};

这里,period 是一个IPeriod 对象,但代码不知道它是DayMonth,还是Year

为了清楚起见,上面的示例使用内部访问者变体,即isomorphic to a Church encoding

动物

使用动物来理解面向对象的编程很少有启发性。我认为学校应该停止使用这个例子,因为它更容易混淆而不是帮助。

OP 代码示例没有遇到访问者模式解决的问题,因此在这种情况下,如果您看不到好处,也就不足为奇了。

CatDog不是异构的。它们具有相同的类字段和相同的行为。唯一的区别在于构造函数。您可以轻松地将这两个类重构为一个 Animal 类:

public class Animal {
    private int health;

    public Animal(int health) {
        this.health = health;
    }

    public void increaseHealth(int healthIncrement) {
        this.health += healthIncrement;
    }

    public int getHealth() {
        return health;
    }
}

然后为猫和狗定义两种创建方法,使用两个不同的health 值。

由于您现在只有一个班级,因此不需要访问者。

【讨论】:

  • 我认为添加一个客户端代码示例(即遍历Periods 的集合)会很有启发性,以明确为什么您不能像建议的那样只拥有例如DayCommand OP。
  • @daniu 正如我所写,链接文章中有一个完整上下文的示例。
  • @daniu 但你是对的,所以我编辑了答案以包含该文章中的一些代码。它确实改善了答案。谢谢你的建议。
  • @BurnsBA GoF = 四人帮Design Patterns 的作者。它也经常被用作书籍本身的转喻。
  • @AFP_555 这是推测性的:如果您的语言具有它所没有的语言特性,可以解决设计模式解决的问题,那么您是否需要该模式? 同义反复:不。这是针对 DP 提出的(尤其是 FP 社区)典型的(并且完全合理的)批评:它解决了“更好”的语言所没有的问题。
【解决方案2】:

Visitor 中的来回是模拟一种double dispatch 机制,根据两个对象的运行时类型选择方法实现。

如果您的动物访问者的类型都是抽象的(或多态的),这很有用。在这种情况下,您有可能从 2 x 2 = 4 种方法实现中进行选择,具体取决于 a) 您想要执行哪种操作(访问),以及 b) 您希望此操作适用于哪种动物。

如果你使用的是具体的和非多态的类型,那么这部分来回确实是多余的。

【讨论】:

  • 这个。双重分派是这里的关键概念,通过将常规方法名称“accept”更改为更像revealYourClassToThisGuy 的名称,可以强调其对访问者模式的重要性。
  • @JohnBollinger 您的评论让我明白了这一点。在创建动物对象时,我尝试使用 Animal 接口而不是具体的动物(狗、猫),当然,代码不会编译。所以... accept 方法的重点是让具体的动物对访问者说“嘿,我是猫,因为我正在对你调用 visitCat 方法。现在你可以像对待猫一样对待我了。”。虽然我还是不明白双发的概念
  • @AFP_555,看看What is Method Dispatch? 可能会有所帮助。在Java 中,这是一个包含编译时和运行时部分的活动。双重分派只是意味着您链接两个方法分派以实现您所追求的,而不仅仅是一个。正是您在问题中批评的“来回疯狂”,这就是使访问者模式起作用的机制。
【解决方案3】:

BACK-AND-FORTH 是这个意思吗?

public class Dog implements Animal {

    //...

    @Override
    public void accept(AnimalAction action) {
        action.visit(this);
    }
}

这段代码的目的是你可以在不知道具体类型的情况下对类型进行调度,如下所示:

public class Main {

    public static void main(String[] args) {
        AnimalAction jumpAction = new JumpVisitor();
        AnimalAction eatAction = new EatVisitor();


        Animal animal = aFunctionThatCouldReturnAnyAnimal();
        animal.accept(jumpAction);
        animal.accept(eatAction);
    }

    private static Animal aFunctionThatCouldReturnAnyAnimal() {
        return new Dog();
    }
}

所以你得到的是:你可以在只知道它是动物的情况下调用对动物的正确个体动作。

这在您遍历复合模式时特别有用,其中叶节点是Animals,内部节点是Animals 的聚合(例如List)。您的设计无法处理List&lt;Animal&gt;

【讨论】:

  • 这里显示的 OP 缺少的原则是,编程到接口,而不是实现。 OP 直接编程到 DogCat 所以没有多态性,因此 OO 设计模式不是很有用。
  • 是啊,没错,我不能把Dog dog当成Animal dog,这样代码编译不出来。这是一个非常奇特的 Java 事情。现在我明白了,来回是完全必要的,即使非常尴尬。谢谢。
  • 是的列表。只是看了一眼 OP 的问题,很明显他们从未在 Cats、Dogs 和以前未知的 Ferret 类列表中看到标准的“for(Animal a : myAnimals) { a.doThing(); }”示例来理解动态调度。
  • @jaco0646 等等...我正在阅读 GoF 的书,他们说:“迭代器不能跨具有不同类型元素的对象结构工作。(...) 访问者可以没有这个限制。它可以访问没有公共父类的对象。您可以将任何类型的对象添加到访问者接口中。MyType 和 YourType 根本不必通过继承相关“..... . 所以.... 我们并不真正需要 Animal 父类,这就是访问者的重点,它能够遍历不属于同一层次结构的不相关对象。那么我为什么要上 Animal 课呢?
  • 不需要父类,但是接口必须提供accept(Visitor visitor)-方法,并且每个遍历的对象都必须实现它。
【解决方案4】:

访问者模式解决了将函数应用于图结构元素的问题。

更具体地说,它解决了访问某个图结构中的每个节点 N 的问题,在某个对象 V 的上下文中,并且对于每个 N,调用某个通用函数 F(V, N)。 F的方法实现是根据V和N的类型来选择的。

在具有多个分派的编程语言中,访问者模式几乎消失了。它简化为图对象的遍历(例如递归树下降),它对每个 N 个节点进行简单的 F(V, N) 调用。完成!

例如在 Common Lisp 中。为简洁起见,我们甚至不定义类:integersstrings 是类,所以让我们使用它们。

首先,让我们编写泛型函数的四个方法,用于访问整数或字符串的整数或字符串的每个组合。这些方法只产生输出。我们没有用defgeneric 定义泛型函数; Lisp 推断出这一点,并为我们隐含地做到了:

(defmethod visit ((visitor integer) (node string))
  (format t "integer ~s visits string ~s!~%" visitor node))

(defmethod visit ((visitor integer) (node integer))
  (format t "integer ~s visits integer ~s!~%" visitor node))

(defmethod visit ((visitor string) (node string))
  (format t "string ~s visits string ~s!~%" visitor node))

(defmethod visit ((visitor string) (node integer))
  (format t "string ~s visits integer ~s!~%" visitor node))

现在让我们使用一个列表作为访问者迭代的结构,并为此编写一个包装函数:

(defun visitor-pattern (visitor list)
  ;; map over the list, doing the visitation
  (mapc (lambda (item) (visit visitor item)) list)
  ;; return  nothing
  (values))

交互式测试:

(visitor-pattern 42 '(1 "abc"))
integer 42 visits integer 1!
integer 42 visits string "abc"!

(visitor-pattern "foo" '(1 "abc"))
string "foo" visits integer 1!
string "foo" visits string "abc"!

好的,这就是访问者模式:遍历结构中的每个元素,并使用访问上下文对象对方法进行双重调度。

“来回疯狂”与在只有单一调度的 OOP 系统中模拟双重调度的样板代码有关,其中方法属于类而不是泛型函数的特化。

因为在主流的单调度OOP系统中,方法是封装在类中的,我们遇到的第一个问题是visit方法在哪里?是在访问者上还是在节点上?

答案证明它必须是两者。我们需要在这两种类型上发送一些东西。

接下来的问题是,在 OOP 实践中,我们需要良好的命名。我们不能在 visitorvisited 对象上都有 visit 方法。当访问的对象被访问时,“访问”动词不用于描述该对象正在做什么。它“接受”访问者。所以我们必须将那一半的操作称为accept

我们创建了一个结构,每个要访问的节点都有一个accept 方法。此方法在节点的类型上调度,并采用Visitor 参数。事实上,该节点有多个accept 方法,它们静态地 专门针对不同类型的访问者:IntegerVisitorStringVisitorFooVisitor。请注意,我们不能只使用String,即使我们在语言中有这样一个类,因为它没有使用visit 方法实现Visitor 接口。

所以发生的情况是我们遍历结构,获取每个节点 N,然后调用 V.visit(N) 让访问者访问它。我们不知道V 的确切类型;这是一个基本参考。每个访问者实现都必须将visit 实现为样板文件(使用非 Java 或 C++ 的伪语言):

StringVisitor::visit(Visited obj)
{
  obj.Accept(self)
}

IntegerVisitor::visit(Visited obj)
{
  obj.Accept(self)
}

原因是self 必须为Accept 调用静态类型化,因为Visited 对象在编译时针对不同类型选择了多个Accept 实现:

IntegerNode::visit(StringVisitor v)
{
   print(`integer @{self.value} visits string @{v.value}`)
}

IntegerNode::visit(IntegerVisitor v)
{
   print(`integer @{self.value} visits string @{v.value}`)
}

所有这些类和方法都必须在某处声明:

class VisitorBase {
  virtual void Visit(VisitedBase);
}

class IntegerVisitor;
class StringVisitor;

class VisitedBase {
  virtual void Accept(IntegerVisitor);
  virtual void Accept(StringVisitor);
}

class IntegerVisitor : inherit VisitorBase {
  Integer value;
  void Visit(VisitedBase);
}

class StringVisitor: inherit VisitorBase {
  String value;
  void Visit(VisitedBase);
}

class IntegerNode : inherit VisitedBase {
  Integer value;
  void Accept(IntegerVisitor);
  void Accept(StringVisitor);
}

class StringNode : inherit VisitedBase {
  String value;
  void Accept(IntegerVisitor);
  void Accept(StringVisitor);
}

这就是单调度静态重载访问者模式:有一堆样板,加上其中一个类,无论是访问者还是访问者,都必须知道所有的静态类型其他受支持的,因此它可以对其进行静态调度,并且对于每种静态类型,也会有一个虚拟方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-11-27
    • 2021-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多