对访客设计模式感到困惑周期示例动物

2022-09-01 02:29:53

所以,我刚刚阅读了关于访客模式的信息,我发现访客和元素之间的来回非常奇怪!

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

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

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

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

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


答案 1

OP 中的代码类似于称为内部访问者的访客设计模式的众所周知的变体(例如,请参阅 Mass 的可扩展性。《对象代数的实用可扩展性》作者:Bruno C. d. S. Oliveira 和 William R. Cook)。但是,该变体使用泛型和返回值(而不是 )来解决访问者模式解决的一些问题。void

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

Visitor 模式解决的主要问题是,当您有异构对象需要处理相同的对象时。正如“四人帮”(设计模式的作者)所说,在以下情况下使用模式:

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

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

周期示例

使用动物领域很少能说明问题(我稍后会回到这一点),所以这是另一个更现实的例子。示例在 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);
    }
}

如果你有 一个 ,你会得到前一个,但如果你有一个,你会得到上一个,依此类推。DayDayMonthMonth

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

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

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

这里, 是一个对象,但代码不知道它是 、 和 还是 .periodIPeriodDayMonthYear

为了清楚起见,上面的示例使用了内部访客变体,该变体与 Church 编码同构

动物

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

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

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

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

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


答案 2

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

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

enter image description here enter image description here

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


推荐