非循环访问者相对于具有开启类型的命令的优势

问题描述

访问者模式在元素层次结构稳定并且操作这些元素所需的功能经常发生变化的情况下很有用。

在元素层次结构发生变化的情况下,访问者模式会受到耦合的影响,这会迫使重新构建元素和功能层次结构中的所有类。

为了改进这一点,非循环访问者使用了一个额外的抽象级别,顶部有一个空的访问者接口,元素层次结构中的每个类都有一个特定的接口。

假设有两个具体的元素类型 IntMessage 和 StringMessage,一个非循环访问者看起来像这样:

abstract class Message // parent for the model/element/data classes
{
    public abstract void Accept(Visitor visitor);
}
class IntMessage : Message  // concrete element type 1
{
    internal int data;

    public override void Accept(Visitor visitor)
    {
        // check if the concrete visitor kNows how to work on IntMessage
        if (visitor is IntMessageVisitor)
            (visitor as IntMessageVisitor).Visit(this);   
    }
}
class StringMessage : Message  // concrete element type 2
{
    internal String msg;
    public override void Accept(Visitor visitor)
    {
        // check if the concrete visitor kNows how to work on StringMessage
        if (visitor is StringMessageVisitor)
            (visitor as StringMessageVisitor).Visit(this);
    }
}


interface Visitor  // empty parent interface for acyclic visitor
{
}

interface IntMessageVisitor : Visitor
{
    void Visit(IntMessage message);
}
interface StringMessageVisitor : Visitor
{
    void Visit(StringMessage message);
}

具体访问者将从它知道如何访问的元素类型的所有特定访问者接口继承。这样做的好处是,在元素层次结构中添加新类的情况下,只有需要访问新元素的具体访问者才会被迫更改。

class PrintVisitor : StringMessageVisitor,IntMessageVisitor
{
        public void Visit(IntMessage message)
        {
            Console.WriteLine("Int message with data = " + message.data);
        }
        public void Visit(StringMessage message)
        {
            Console.WriteLine("String message with data = " + message.msg);
        }
}

足够的设置,让我们继续问题。

问题是,鉴于非循环访问者模式的复杂性,与使用带有开关类型的简单命令相比,它有什么真正的好处吗?

例如,我们可以将 PrintVisitor 重写为以下打印命令:

class PrintCommand : Command
{
    public void Execute(Message message)
    {
        // switch on type
        if (message.GetType() == typeof(IntMessage))
        {
            Console.WriteLine("Int message with data = " + ((IntMessage)message).data);
        }
        else
        if (message.GetType() == typeof(StringMessage))
        {
            Console.WriteLine("String message with data = " + ((StringMessage)message).msg);
        }
    }
}

如果将来将新类添加到元素层次结构中(例如 DateMessage ),那么仍然只有想要处理新元素类型的命令需要更改。最终的设计会更简单,没有多重接口继承和双重分派,代价是使用运行时类型信息而不是虚函数

似乎就 OCP 和未来的维护而言,非循环访问者的切换类型没有额外的成本。

是否有任何理由更喜欢 ACyclic Visitor 而不是带有 switch-on-type 的命令?

解决方法

没有明确的答案。答案取决于您的具体情况。你的例子相对简单,因为它只涉及单一类型的替代品,而不是替代系列类型。 Glue 展示了使用不同的访问者来处理树中多个不同类型的族。一个很好的例子是将 XML 呈现为 HTML、PDF 或 Word 文档的访问者。每个访问者都处理一系列元素。

在简单的示例中,Visitor 没有那么有用。它以(显着的)复杂性为代价增加了类型安全性。

由于 C# 6 引入了模式,所有这些代码都可以大大减少。在 C# 9 Execute 中可以是:

public static void Execute(Message message)
{
    var text=message switch { 
                 IntMessage intM=>$"blah {intM.data}",StringMessage stM=> $"blah {stM.msg}",_ => throw new ArgumentException($"Unknown type {message.GetType()}",nameof(message))
    };
    Console.WriteLine(text);
}

先是泛型,然后是 dynamic,现在模式匹配等功能特性极大地减少了需求,同时大大简化了代码很多。函数式语言很少需要访问者,因为编译器可以轻松推断类型并检测遗漏情况。

如果 C# 有区分联合,这是一个热切期待但从 C# 7 开始一直推迟的功能,那么甚至不需要默认情况。编译器本身会识别缺失的情况。

在此示例中,更改可以本地化为仅此方法。

不过,在渲染器示例中,每个具体访问者都必须处理相同的众所周知的元素集。这些元件具有众所周知的结构。在这种情况下是否需要访客?

也许吧,但它可以简化很多

模式匹配等功能特性可以轻松处理多种元素类型多个渲染器。对元素结构的任何更改都可以限制在模式匹配代码中,将特定于渲染器的调用转发给具体的渲染器访问者。

除非它不能。在最简单的情况下,某些 渲染器可能需要以不同的方式处理元素。某些渲染器可能需要多次访问元素。

或者数据的大小可能需要不同的策略。当想要导出 1M 行时,最好的选择是流式传输渲染结果而不是将它们缓存在内存中。 Excel 虽然是一个 ZIP 包,因此需要先收集结果才能导出。在这种情况下,即使结构和渲染器相同,但不同的数据大小需要不同的实现。

,

Bob Martin 早在 2005 年就在博客中讨论了这个问题:http://butunclebob.com/ArticleS.UncleBob.VisitorVersusInstanceOf。关于类型检查(非访问者)方法,他说,

...有些事情我不喜欢这个。一方面,随着越来越多的导数被添加到系统中,if/else 语句链将无限增长。执行 [type-checking] 语句需要时间,平均而言,我们将不得不执行其中的一半……这意味着运行时复杂度为 O(n),其中 n 是导数的数量。>

他提到的派生类是“可访问的”子类,在 OP 中将是 Message 的子类。他的论点归结为表现。这并不意味着非循环访问者总是比简单的旧类型检查更可取。

我并不是要说明应该始终使用非循环访问者。事实上,我们讨论过的每种形式都适用于各种情况。我使用以下决策表:

上下文层次结构变化 访问者层次结构变化 效率很重要
if/else of instanceof 没有 没有 没有
访客 没有 是的 是的
非循环访问者 是的 是的 是的

将类型检查逻辑隐藏在 OP 中所示的抽象背后可以解决有关层次结构更改的问题,这意味着唯一剩下的问题是性能。与 if/else 链中的许多相比,非循环访问者只需要一种类型检查。