访问者设计模式和多层类层次结构

3

我有五个带访问者的类:

struct Visitor
{
    virtual ~Visitor() = default;
    virtual void visit(A&) {}
    virtual void visit(B&) {}
    virtual void visit(C&) {}
    virtual void visit(D&) {}
    virtual void visit(E&) {}
};

struct A
{
    virtual ~A() = default;
    virtual void accept(Visitor& v) { v.visit(*this); }
};
struct B : A { void accept(Visitor& v) override { v.visit(*this); } };
struct C : A { void accept(Visitor& v) override { v.visit(*this); } };
struct D : C { void accept(Visitor& v) override { v.visit(*this); } };
struct E : C { void accept(Visitor& v) override { v.visit(*this); } };

所有实例在用户代码中尽可能以最高抽象级别显示,因此它们都将被视为 A&。用户代码需要执行两种类型的操作:
  1. 如果实例恰好是类型 C,则打印 "I am C"
  2. 如果实例是类型 C 或任何其子类型(即 DE),则打印 "I am C"
操作 1 的实现很简单,几乎可以直接使用访问者设计模式框架提供的基础设施来完成:
struct OperationOne : Visitor
{
    void visit( C& ) override { std::cout << "I am C" << std::endl; }
};

正如预期的那样,字符串"I am C"只会被打印一次:

int main( )
{
    A a; B b; C c; D d; E e;
    std::vector<std::reference_wrapper<A>> vec = { a, b, c, d, e };

    OperationOne operation_one;

    for (A& element : vec)
    {
        element.accept(operation_one);
    }
}

问题是:对于第二个操作,整个基础架构都不再起作用了,假设我们不想再次重复DE的打印代码。

演示

struct OperationTwo : Visitor
{
    void visit( C& ) override { std::cout << "I am C" << std::endl; }
    void visit( D& ) override { std::cout << "I am C" << std::endl; }
    void visit( E& ) override { std::cout << "I am C" << std::endl; }
};

尽管这种方法可能有效,但如果层级结构发生变化,D 不再是 C 的子类型,而是直接成为 A 的子类型,那么这段代码仍然可以编译通过,但在运行时不会产生预期的行为,这是危险和不可取的。
要实现操作2的一种解决方案是更改访问者架构,使得每个可访问类都将接受的访问者传递到其基类:
struct B : A
{
    void accept(Visitor& v) override
    {
        A::accept( v );
        v.visit( *this );
    }
};

如果继承层次结构发生变化,编译器在尝试传播已接受的访问者时将无法找到基类,从而导致编译错误。

话虽如此,我们现在可以编写第二个操作访问者,这次我们不需要为DE再次复制打印代码:

struct OperationTwo : Visitor
{
    void visit(C&) override { std::cout << "I am C" << std::endl; }
}

当使用OperationTwo时,用户代码中预计会打印三次字符串"I am C":

int main()
{
    A a; B b; C c; D d; E e;
    vector< reference_wrapper< A > > vec = { a, b, c, d, e };

    OperationTwo operation_two;

    for ( A& element : vec ) 
    {
        element.accept( operation_two );
    }
}

演示

等等: OperationOneOperationTwo 的代码完全相同!这意味着通过更改第二个操作的基础设施,我们基本上破坏了第一个操作。实际上,现在也会有 OperationOne 打印三次字符串 "I am C"

为了使 OperationOneOperationTwo 无缝地共同工作,可以采取什么措施?我需要将访问者设计模式与另一种设计模式结合使用,还是根本不需要使用访问者?


第一个操作在设计上并不是很合理。你为什么想要知道任何东西的确切类型呢?这将违反LSP原则。(我不是在谈论基础类型代码,比如序列化,而是应用程序级别的逻辑)。 - n. m.
1
对我来说,这很有道理。我们使用这样的访问者作为场景图的遍历器 - 用于各种操作。因此,我知道两种情况都是可以想象的。(因此,我对这个问题很感兴趣 - 可能会有回报。);-) - Scheff's Cat
现在看起来可能是有道理的。这样的设计通常很脆弱,在层次结构中进行更多或更少的重大变化时会崩溃。如果您决定将C分成两个类,或将其作为具体子类的抽象类,或者其他任何操作,您的设计应该继续工作。当然,如果这对您有效,您可以选择使用它。 - n. m.
1
@n.m 我部分地同意。抽象基类的技巧确实是我曾经用过的解决方案。另一方面,场景图上的必要操作随着需求和用例数量的增长而不断发展。很难预见每个具体要求并在数据设计中考虑它。如果操作导致矛盾的要求,甚至可能是不可能的... - Scheff's Cat
1个回答

4
您可以使用以下内容作为访问者,该内容将通过重载决议进行调度:
template <typename F>
struct OverloadVisitor : Visitor
{
    F f;

    void visit(A& a) override { f(a); }
    void visit(B& b) override { f(b); }
    void visit(C& c) override { f(c); }
    void visit(D& d) override { f(d); }
    void visit(E& e) override { f(e); }
};

然后

struct IAmAC
{
    void operator()( C& ) { std::cout << "I am C" << std::endl; }
    void operator()( A& ) {} // Fallback
};

using OperationTwo = OverloadVisitor<IAmAC>;

演示


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