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

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

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

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

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

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

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 中的代码类似于著名的访问者设计模式的变体,称为内部访问者(参见例如,可扩展性。布鲁诺 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)

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

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

为此,定义一个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,依此类推。

您可以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对象,但代码不知道它是 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;
    }
}

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

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

  • I think it would be instructive to add an example of client code, ie iterating through a collection of `Period`s, to make clear why you can't just have eg `DayCommand` as suggested by OP.

回答

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

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

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

  • This. Double dispatch is the key concept here, and its importance to the Visitor pattern would be emphasized by changing the conventional method name "`accept`" to something more like `revealYourClassToThisGuy`.
  • @AFP_555, it might help to have a look at [What is Method Dispatch?](https://stackoverflow.com/a/1811769/2402272) In Java, this is an activity with both compile-time and run-time parts. Double dispatch just means that you chain two method dispatches to achieve what you're after instead of just one. It is exactly the "back and forth madness" you criticize in the question, and this is the mechanism that makes the Visitor pattern work.

回答

来回,你是这个意思吗?

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 而内部节点是 的聚合(例如 a List),这将特别有用Animals。AList<Animal>不能与您的设计一起处理。

  • The principle shown here that is missing from the OP is, *Program to an interface, not an implementation.* The OP is programming directly to `Dog` and `Cat` so there is no polymorphism, and thus OO design patterns are not very useful.
  • Yeha, that's right, I can't treat Dog dog as Animal dog, the code won't compile that way. This is a very peculiar Java thing. Now I understand that the back and forth is completely necessary, even if it is extremely awkward. Thanks.

以上是对访问者设计模式感到困惑的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>