使用访问者设计模式的好处是什么?

5

在你告诉我已经有了一个类似的问题之前,是的,我知道,我已经读过。但那里的问题集中在何时使用访问者设计模式,而我对为什么感兴趣。

我知道这些东西是如何运作的。经典的动物,狗,猫例子总是能很好地解释清楚。

问题是这段代码:

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}

对我来说,这似乎很不自然。为什么?

我的意思是,是的,这样我就有了我的Dog和Cat模型未区分(这是我第一次在英语中使用这个词),因为真正的实现被隐藏在Sound类下面,但这不仅仅是一种拖累你代码的方式吗?难道多态性不足以做到这一点吗?

对我来说,不同之处在于使用多态性需要编辑每个类(但模型保持不变,对吧?),而使用访问者设计模式只需要编辑一个类。

4个回答

8
访问者模式使您能够做一些事情,仅依靠多态性是无法做到的:处理未预期的用例。如果您正在编写库,则这是一个重要的点。让我详细说明一下:
考虑一个使用访问者模式的经典示例,即对某些抽象语法树节点的操作。为了添加一些细节,假设您刚刚为SQL编写了解析器库,它接受字符串、解析它们,并返回输入中找到的AST。除非您可以预见到客户端代码可能对此类AST具有的所有潜在用例,否则您必须提供一种“通用”方式来遍历AST。提供类似DOM的访问器函数(getNodeTypegetParentNodegetPreviousNode)是一种方式。问题在于,这会给您的库客户带来沉重的负担,因为他们需要自己进行调度。更重要的是,他们需要详细了解每个可能的节点类型要跟随哪些指针:
void 
walk_tree(AstNode* node) 
{
    switch( node->getNodeType() ) {
    case SELECT_NODE:
        for( AstNode* child = node->getFirstChild(); child; child = child->getNextNode() ) {
             walk_tree(child);
        }
        break;
    ...
    }
}

访问者模式将这个负担从客户端转移到了库中。

确切地说,这种行为可以由第三方实现。 - Kirk Woll
好的,但是如果你为SQL定义解析器,那么你已经有了语法定义吗?哪些是未预料到的用例?实际上,既然我们正在谈论解析,难道解析器生成器不应该是更合适的例子吗?在那里,您拥有任意语法,因此必须定义一个通用的树遍历器类。 - dierre
这不是关于 SQL 结构的问题,而是关于客户端如何使用您的库的结果/输出。解析器示例仅仅是因为它有些“经典”。顺便说一下,如果您拥有一个完全通用的 API(任意“DOM”-like 节点)而没有固定的结构,则使用通用访问器方法可能比访问者模式更好。 - Dirk

3

假设你已经在一个你不拥有的库中定义了一些基本的东西,而你需要扩展它。比如:

// In base lib:
interface ISomething {
    void DoSomething();
}

class Something1 : ISomething {
    // ...
}

class Something2 : ISomething {
    // ...
}

多态性允许您定义可以执行操作的新事物:
// In your lib:
class MySomething : ISomething {
}

现在基本库可以与您的 MySomething 一起使用,就像它自己定义一样。它不允许您添加新的操作DoSomething是我们可以使用ISomething的唯一操作。访问者模式解决了这个问题。
缺点是使用访问者模式会消耗您刚刚展示的定义新类型的能力。大多数语言都允许您轻松添加操作或类型,但不能同时进行,这被称为表达式问题
访问者模式很酷,但我从未在实现编译器之外找到过使用它的需要。

你不能添加新类型,因为你的 Visitor 仍将是库的一个组件,对吧? - dierre
1
访问者类将在基础库中定义,并且将具有一组固定的方法,每种类型一个。您无法在自己的库中添加新类型,因为您无法更改访问者,并且您无法将访问者移动到自己的库中,因为基础库中的类型需要在其accept()方法中引用它。 - munificent

2
访问者模式非常有用。
至少有三个重要的原因可以使用它:
1. 减少代码的繁殖,当数据结构改变时,只需稍作修改即可。 2. 对多个数据结构应用相同的计算,而不需要更改实现计算的代码。 3. 在不更改遗留代码的情况下向遗留库添加信息。
请查看我写的一篇文章(链接)
干杯

1
当我有一棵对象树,需要以多种方式打印内容时,我使用了访问者模式。无论是逗号分隔、XML还是其他任何输出格式,我都创建了CommaSepVisitor、XMLVisitor和HTMLVisitor类,而不是为每个输出格式向树类添加新的打印方法。由于我使用了访问者模式,并创建了更多的Visitor类型,所以树代码从未改变,因此我没有引入错误。编写访问者本身很容易。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接