java - 对访客设计模式感到困惑

标签 java design-patterns visitor-pattern

所以,我只是在阅读访问者模式,发现访问者和元素之间的来回非常奇怪!
基本上我们称之为元素,我们将其传递给访问者,然后元素将自身传递给访问者。然后访问者操作元素。什么?为什么?感觉太没必要了。我称之为“来回疯狂”。
因此,当需要在所有元素上实现相同的操作时,访问者的目的是将元素与其操作分离。这是为了防止我们需要用新的 Action 来扩展我们的元素,我们不想进入所有这些类并修改已经稳定的代码。所以我们在这里遵循开放/封闭原则。
为什么会有这么多来回,如果我们没有这些,我们会失去什么?
例如,我编写的这段代码牢记了这个目的,但跳过了访问者模式的疯狂交互。基本上我有会跳和吃的动物。我想将这些 Action 与对象分离,因此我将这些 Action 移至访客。进食和跳跃会增加动物的健康(我知道,这是一个非常愚蠢的例子......)

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

最佳答案

OP 中的代码类似于众所周知的访问者设计模式的变体,称为内部访问者(参见例如,Bruno C. d. S. Oliveira 和 William R. Cook 的 Extensibility for the Masses. Practical Extensibility with Object Algebras)。然而,这种变体使用泛型和返回值(而不是 void)来解决访问者模式解决的一些问题。
那是哪个问题,为什么 OP 变化可能不够?
访问者模式解决的主要问题是当您有需要处理的异构对象时。正如四人帮(Design Patterns 的作者)所说,您在以下情况下使用该模式

"an object structure contains many classes of objects with differing interfaces, and you want to perform operations on these objects that depend on their concrete classes."


这句话缺少的是,虽然您希望“对这些依赖于其具体类的对象执行操作”,但您希望将这些具体类视为具有单个多态类型。
一个时期的例子
使用动物域很少是说明性的(我稍后会谈到),所以这里有另一个更现实的例子。示例在 C# 中 - 我希望它们对您仍然有用。
想象一下,您正在开发一个在线餐厅预订系统。作为该系统的一部分,您需要能够向用户显示日历。该日历可以显示给定日期的剩余座位数,或列出当天的所有预订。
有时,您希望显示一天,但在其他时候,您希望将整个月份显示为单个日历对象。投入一整年以获得良好的衡量标准。这意味着您有三个时期:年、月和日。每个都有不同的接口(interface):
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);
}
并使每个类实现接口(interface)。这里是 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);
    }
}
这使您能够将三个异构类视为单一类型,并在该单一类型上定义操作,而无需更改接口(interface)。
例如,这里是一个计算前一周期的实现:
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 , 等等。
你可以看到PreviousPeriodVisitor this article 中使用的类(class)和其他访客,但这里是使用它们的几行代码:
var previous = period.Accept(new PreviousPeriodVisitor());
var next = period.Accept(new NextPeriodVisitor());

dto.Links = new[]
{
    url.LinkToPeriod(previous, "previous"),
    url.LinkToPeriod(next, "next")
};
在这里,periodIPeriod对象,但代码不知道它是否是 Day , 和 Month , 或 Year .
为了清楚起见,上面的示例使用了内部访问者变体,即 isomorphic to a Church encoding .
动物
使用动物来理解面向对象编程很少有启发性。我认为学校应该停止使用这个例子,因为它更容易混淆而不是帮助。
OP 代码示例没有遇到访问者模式解决的问题,因此在这种情况下,如果您看不到好处,也就不足为奇了。CatDog类不是异质的。它们具有相同的类字段和相同的行为。唯一的区别在于构造函数。您可以轻松地将这两个类重构为一个 Animal类(class):
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值(value)观。
由于您现在只有一个类(class),因此不需要访问者。

关于java - 对访客设计模式感到困惑,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67815554/

相关文章:

java - 通过hibernate插入2.对象后出现Hsql错误

java.lang.OutOfMemoryError:为 ChunkPool::allocate 请求了 32756 字节。交换空间不足?

java - 显示从edittext到textview的文本

Java:将对象链接到另一个对象中的变量

java - 使空对象不可变的任何标准模式或策略

visitor-pattern - 访客行为模式的优点和缺点是什么?

java - 通过反射获取 Java 字段,而不是通过其 String 名称

java - 如果没有观察者和主体的接口(interface),它仍然是观察者模式吗?这是一个我不确定的例子

design-patterns - 为什么访问者负责在访问者模式中枚举子级?

java - 重载方法分派(dispatch),无访问者模式