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

AFP*_*555 38 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());
    }
}

Run Code Online (Sandbox Code Playgroud)

Mar*_*ann 35

OP 中的代码类似于著名的访问者设计模式的变体,称为内部访问者(参见例如,可扩展性。布鲁诺 C. d. S. 奥利维拉和威廉 R. 库克的对象代数可扩展性)。然而,这种变体使用泛型和返回值(而不是void)来解决访问者模式解决的一些问题。

那是哪个问题,为什么 OP 变化可能不足?

访问者模式解决的主要问题是当您需要处理异类对象时。正如四人帮Design Patterns的作者)所说,您在以下情况下使用该模式

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

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

一个时期的例子

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

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

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

public Year(int year)

public Month(int year, int month)

public Day(int year, int month, int day)
Run Code Online (Sandbox Code Playgroud)

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

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

为此,定义一个IPeriod接口:

internal interface IPeriod
{
    T Accept<T>(IPeriodVisitor<T> visitor);
}
Run Code Online (Sandbox Code Playgroud)

并使每个类都实现接口。这是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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

dto.Links = new[]
{
    url.LinkToPeriod(previous, "previous"),
    url.LinkToPeriod(next, "next")
};
Run Code Online (Sandbox Code Playgroud)

这里,period是一个IPeriod对象,但代码不知道它是 a Day、 andMonth还是 a Year

需要明确的是,上面的示例使用了内部访问者变体,它与 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

由于您现在只有一个类,因此不保证访问者。

  • 我认为添加客户端代码的示例(即迭代“Period”集合)是有启发性的,以明确为什么您不能像OP建议的那样只拥有“DayCommand”。 (2认同)

SDJ*_*SDJ 23

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

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

在此处输入图片说明 在此处输入图片说明

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

  • 这。双重调度是这里的关键概念,通过将传统方法名称“accept”更改为更像“revealYourClassToThisGuy”的名称,可以强调它对访问者模式的重要性。 (9认同)
  • @AFP_555,看看[什么是方法调度?](/sf/answers/126823861/)可能会有所帮助。在Java中,这是一个同时具有编译时和运行时部分的活动。双重分派只是意味着您链接两个方法分派来实现您想要的目标,而不是仅使用一个方法分派。这正是您在问题中批评的“来回疯狂”,这就是使访客模式发挥作用的机制。 (2认同)

Cor*_*onA 7

来回,你是这个意思吗?

public class Dog implements Animal {

    //...

    @Override
    public void accept(AnimalAction action) {
        action.visit(this);
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码的目的是你可以在不知道具体类型的情况下分派类型,如下所示:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

如果您遍历复合模式,其中叶节点是Animals 而内部节点是 的聚合(例如 a List),这将特别有用Animals。AList<Animal>不能与您的设计一起处理。

  • 这里展示的 OP 中缺少的原则是,*对接口进行编程,而不是实现。* OP 直接对“Dog”和“Cat”进行编程,因此不存在多态性,因此 OO 设计模式不是很好有用。 (4认同)
  • 是的,没错,我不能把 Dog 狗当作 Animal 狗,代码不会那样编译。这是Java 中一个非常奇特的东西。现在我明白了,来回是完全必要的,即使是极其尴尬的。谢谢。 (2认同)