1.More C++ Idioms - Acyclic Visitor Pattern惯用法and more

enter image description here

More C++ Idioms wikibook

Acyclic Visitor Pattern(无环访问者模式)

Acyclic Visitor Pattern(无环访问者模式)是对传统访问者模式(Visitor Pattern)的一种改进,旨在避免访问者模式中的循环依赖问题。传统的访问者模式依赖于一组“元素”类和一个“访问者”类,这些元素类通常会接受访问者的访问,并通过“接受”方法(accept)调用访问者的方法。然而,在复杂的设计中,如果元素类和访问者类彼此依赖,就会产生一个环状依赖关系,这对于系统的扩展和维护是非常不利的。

传统的访问者模式问题

在传统的访问者模式中,通常元素类会有一个接受访问者的方法(accept()),而每个访问者又会对不同类型的元素类进行操作。问题在于,当访问者的接口依赖于元素类的具体实现时,就会产生循环依赖。

例如,假设你有多个类(比如 ElementAElementB),每个类都接受不同的访问者(比如 Visitor1Visitor2),如果 Visitor1 依赖于 ElementA 并且 Visitor2 依赖于 ElementB,而 Visitor2 又想访问 ElementA,那么会出现循环引用的情况。这样的依赖会导致系统变得难以扩展和维护。

Acyclic Visitor Pattern 的解决方法

无环访问者模式通过将访问者和元素类的关系解耦来避免这种环状依赖。具体来说,它通过引入双重分派(double dispatch的技术来确保访问者只依赖于接口,而不是具体的元素类型,从而避免了元素类之间的循环依赖。

关键点:

  • 接口化的访问者:在无环访问者模式中,元素类通过接口来定义接受访问者的方法,而不是直接暴露具体的实现。这意味着访问者只知道元素类的接口,而不需要关心具体类型。

  • 双重分派:无环访问者模式使用双重分派,即元素类通过接受访问者的接口方法来“传递”自己,而访问者则根据元素类型实现不同的操作。这种方式可以确保不产生循环依赖。

  • 避免元素类的实现依赖:元素类和访问者之间的依赖关系只发生在接口层面,不会直接涉及到具体的类实现,从而避免了循环引用。

代码示例

假设你有两个元素类 ElementAElementB,以及一个访问者类 Visitor。传统的访问者模式可能会有一个元素类直接调用访问者的特定方法,而无环访问者模式则避免这种设计。

// Acyclic Visitor Pattern Example

// Forward declaration of Visitor interface
class Visitor;

// Element base class with accept method
class Element {
public:
    virtual ~Element() = default;
    virtual void accept(Visitor& v) = 0;
};

// Concrete ElementA class
class ElementA : public Element {
public:
    void accept(Visitor& v) override;
    void operationA() {
        std::cout << "ElementA operation." << std::endl;
    }
};

// Concrete ElementB class
class ElementB : public Element {
public:
    void accept(Visitor& v) override;
    void operationB() {
        std::cout << "ElementB operation." << std::endl;
    }
};

// Visitor interface
class Visitor {
public:
    virtual void visit(ElementA& elementA) = 0;
    virtual void visit(ElementB& elementB) = 0;
};

// Concrete Visitor implementation
class ConcreteVisitor : public Visitor {
public:
    void visit(ElementA& elementA) override {
        std::cout << "Visiting ElementA" << std::endl;
        elementA.operationA();
    }

    void visit(ElementB& elementB) override {
        std::cout << "Visiting ElementB" << std::endl;
        elementB.operationB();
    }
};

// ElementA's accept method
void ElementA::accept(Visitor& v) {
    v.visit(*this);
}

// ElementB's accept method
void ElementB::accept(Visitor& v) {
    v.visit(*this);
}

int main() {
    ElementA elementA;
    ElementB elementB;
    ConcreteVisitor visitor;

    elementA.accept(visitor);  // Visiting ElementA
    elementB.accept(visitor);  // Visiting ElementB

    return 0;
}

总结:

无环访问者模式通过引入接口和双重分派的机制,避免了传统访问者模式中可能产生的循环依赖问题。这使得在增加新的元素类型或访问者类型时,系统的扩展性和维护性得到了提升。

扩展思考

注意到这个例子 visitor和element相互之间有耦合,如果希望将visitor和element彻底接口: 如果想新增一个element,只需要新增一个element的定义与实现,并不希望去修改visitor 要完全消除每次添加新元素时修改 Visitor 类的做法,并希望在不修改 Visitor 类的情况下增加新元素,我们可以结合 组合模式 (Composite Pattern) 和 访问者模式 (Visitor Pattern) 来实现。

核心目标:

  • 增加新元素时,不修改 Visitor 类。
  • 利用组合模式,让每个元素本身就能够处理它自己的访问,而不需要在 Visitor 类中进行扩展。 思路:
  • 将 Element 的处理逻辑内嵌到每个 Element 中,通过 accept 方法将操作转交给 Element 本身。
  • 通过 多态 和 委托,在每个元素类中通过一个内部的访问操作来处理自己的业务逻辑,而不需要依赖外部的 Visitor 类来处理。

方案设计:

每个 Element 对象会包含自己的操作方法,并且在 accept 方法中将操作委托给对应的处理函数,避免在 Visitor 中增加新的 visit 方法。 我们将访问者的角色从原来的一种集中式的类(即 Visitor)转变为每个元素自身的内部处理逻辑,使得当增加新元素时,不需要修改 Visitor 类的接口。 就这个思路,和ChatGPT聊了半天,最终结果:

#include <iostream>
#include <memory>
#include <vector>

// Base Element class
class Element {
public:
    virtual ~Element() = default;
    virtual void accept() = 0;  // Accept method that delegates to internal processing
};

// Concrete ElementA, B, C, D classes
class ElementA : public Element {
public:
    void accept() override {
        operationA();  // ElementA handles its own logic
    }

    void operationA() {
        std::cout << "ElementA operation." << std::endl;
    }
};

class ElementB : public Element {
public:
    void accept() override {
        operationB();  // ElementB handles its own logic
    }

    void operationB() {
        std::cout << "ElementB operation." << std::endl;
    }
};

class ElementC : public Element {
public:
    void accept() override {
        operationC();  // ElementC handles its own logic
    }

    void operationC() {
        std::cout << "ElementC operation." << std::endl;
    }
};

//class ElementD : public Element {
//public:
//    void accept() override {
//        operationD();  // ElementD handles its own logic
//    }
//
//    void operationD() {
//        std::cout << "ElementD operation." << std::endl;
//    }
//};

// New Visitor class (doesn't need to change when adding new elements)
class Visitor {
public:
    // The visitor class is still useful if needed for specialized operations,
    // but adding a new element doesn't require any changes here.
    virtual void visit() {
        std::cout << "Visiting all elements" << std::endl;
    }
};

// New VisitorWithElements class that uses accept on all elements
class VisitorWithElements {
private:
    std::vector<std::unique_ptr<Element>> elements;

public:
    // Adds an element to the visitor container
    template <typename ElementType>
    void addElement(std::unique_ptr<ElementType> element) {
        elements.push_back(std::move(element));
    }

    // Accept method that applies the accept method of all elements
    void accept() {
        for (auto& element : elements) {
            element->accept();  // Each element handles its own operation
        }
    }
};

int main() {
    // Create a visitor with elements
    VisitorWithElements visitorWithElements;
    visitorWithElements.addElement(std::make_unique<ElementA>());
    visitorWithElements.addElement(std::make_unique<ElementB>());
    visitorWithElements.addElement(std::make_unique<ElementC>());
    //visitorWithElements.addElement(std::make_unique<ElementD>());

    // Accept all elements
    visitorWithElements.accept();

    return 0;
}

注意到,如果希望新增一个元素,比如ElementD,可以看到只需要新建一个ElementD元素定义与实现,并不需要修改其他类,比较完美了

评论