首页 > 解决方案 > 对访客设计模式感到困惑

问题描述

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

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

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

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

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

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

标签: javadesign-patternsvisitor-pattern

解决方案


OP 中的代码类似于众所周知的访问者设计模式的变体,称为内部访问者(参见例如,Bruno C. d. S. Oliveira 和 William R. Cook的面向大众的可扩展性。对象代数的实用可扩展性)。然而,这种变体使用泛型和返回值(而不是void)来解决访问者模式解决的一些问题。

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

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

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

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

一个时期的例子

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

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

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

public Year(int year)

public Month(int year, int month)

public Day(int year, int month, int day)

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

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

为此,请定义一个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);
    }
}

如果你有 a Day,你会得到 previous Day,但如果你有 a Month,你会得到 previous 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

需要明确的是,上面的示例使用了 Internal Visitor 变体,它与 Church 编码同构

动物

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

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然后使用两个不同的值为猫和狗定义两种创建方法。

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


推荐阅读