对访问者设计模式感到困惑

时间:2021-06-03 04:54:39

标签: java design-patterns visitor visitor-pattern

所以,我刚刚阅读了访问者模式,我发现访问者和元素之间的来回非常奇怪!

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

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

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

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

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

4 个答案:

答案 0 :(得分:34)

OP 中的代码类似于著名的访问者设计模式变体,称为内部访问者(参见例如Extensibility for the Mass。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 值为猫和狗定义两种创建方法。

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

答案 1 :(得分:23)

访问者中的来回是模拟一种double dispatch机制,您可以根据两个对象的运行时类型选择一个方法实现。< /p>

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

enter image description here enter image description here

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

答案 2 :(得分:7)

来回,你的意思是这个吗?

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

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

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

答案 3 :(得分:1)

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

更具体地说,它解决了在某个图结构中,在某个对象 V 的上下文中访问每个节点 N 的问题,并且对于每个 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 系统中模拟双分派的样板代码有关,其中方法属于类而不是泛型函数的特化。

>

因为在主流的single-dispatch OOP系统中,方法都是封装在类中的,所以我们遇到的第一个问题就是visit方法住在哪里?是在访问者上还是在节点上?

答案是两者皆有。我们需要在这两种类型上发送一些东西。

接下来的问题是,在OOP实践中,我们需要好的命名。我们不能同时对 visitvisitor 对象使用 visited 方法。当访问对象被访问时,“访问”动词不用于描述该对象正在做什么。它“接受”访客。所以我们必须调用那一半的动作 accept

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

所以发生的事情是我们遍历结构,获取每个节点 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);
}

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