【第20节】C++设计模式(行为模式)-Visitor(访问者)模式

一、问题引出

在面向对象系统的开发和设计过程中,经常会遇到需求变更(Requirement Changing)的情况。通常,我们已经完成了一个设计并实现了系统原型,但客户又提出了新的需求。为了满足这些新需求,我们不得不修改已有的设计。最常见的解决方案是给已经设计好的类添加新的方法,但这会导致设计不断被打补丁,系统代码频繁重新编译,最终使得设计难以封闭,代码难以维护。

Visitor 模式提供了一种优雅的解决方案:将更新(变更)封装到一个类中(访问操作),并由待更改类提供一个接收接口,从而实现需求变更的灵活扩展。

二、模式选择

我们通过 Visitor 模式解决上述问题。Visitor 模式的典型结构图如下:

Visitor 模式的核心思想是 **双分派(Double-Dispatch)技术**。C++ 语言本身支持的是单分派,但通过 Visitor 模式,我们可以实现双分派的效果。

在 Visitor 模式中,`Accept()` 操作是一个双分派的操作。具体调用哪一个 `Accept()` 实现,取决于两个因素:

(1)Element 的类型:因为 `Accept()` 是多态操作,需要具体的 `Element` 子类来决定调用哪个 `Accept()` 实现。

(2)Visitor 的类型:`Accept()` 操作有一个参数(`Visitor* vis`),需要根据实际传入的 `Visitor` 类型来决定调用哪个 `VisitConcrete()` 实现。

三、代码实现

下面我们将通过一个完整的 C++ 代码示例来展示如何实现 Visitor 模式。

代码片段 1:Visitor.h

cpp 复制代码
// Visitor.h
#ifndef _VISITOR_H_
#define _VISITOR_H_

class ConcreteElementA;
class ConcreteElementB;

class Visitor {
public:
    virtual ~Visitor() {}
    virtual void VisitConcreteElementA(ConcreteElementA* elmA) = 0;
    virtual void VisitConcreteElementB(ConcreteElementB* elmB) = 0;
protected:
    Visitor() {}
};

class ConcreteVisitorA : public Visitor {
public:
    ConcreteVisitorA() {}
    virtual ~ConcreteVisitorA() {}
    virtual void VisitConcreteElementA(ConcreteElementA* elmA);
    virtual void VisitConcreteElementB(ConcreteElementB* elmB);
};

class ConcreteVisitorB : public Visitor {
public:
    ConcreteVisitorB() {}
    virtual ~ConcreteVisitorB() {}
    virtual void VisitConcreteElementA(ConcreteElementA* elmA);
    virtual void VisitConcreteElementB(ConcreteElementB* elmB);
};

#endif //~_VISITOR_H_

代码片段 2:Visitor.cpp

cpp 复制代码
// Visitor.cpp
#include "Visitor.h"
#include "Element.h"
#include <iostream>
using namespace std;

Visitor::Visitor() {
    // 构造函数
}

Visitor::~Visitor() {
    // 析构函数
}

void ConcreteVisitorA::VisitConcreteElementA(ConcreteElementA* elmA) {
    cout << "ConcreteVisitorA is visiting ConcreteElementA..." << endl;
}

void ConcreteVisitorA::VisitConcreteElementB(ConcreteElementB* elmB) {
    cout << "ConcreteVisitorA is visiting ConcreteElementB..." << endl;
}

void ConcreteVisitorB::VisitConcreteElementA(ConcreteElementA* elmA) {
    cout << "ConcreteVisitorB is visiting ConcreteElementA..." << endl;
}

void ConcreteVisitorB::VisitConcreteElementB(ConcreteElementB* elmB) {
    cout << "ConcreteVisitorB is visiting ConcreteElementB..." << endl;
}

代码片段 3:Element.h

cpp 复制代码
// Element.h
#ifndef _ELEMENT_H_
#define _ELEMENT_H_

class Visitor;

class Element {
public:
    virtual ~Element() {}
    virtual void Accept(Visitor* vis) = 0;
protected:
    Element() {}
};

class ConcreteElementA : public Element {
public:
    ConcreteElementA() {}
    virtual ~ConcreteElementA() {}
    virtual void Accept(Visitor* vis);
};

class ConcreteElementB : public Element {
public:
    ConcreteElementB() {}
    virtual ~ConcreteElementB() {}
    virtual void Accept(Visitor* vis);
};

#endif //~_ELEMENT_H_

代码片段 4:Element.cpp

cpp 复制代码
// Element.cpp
#include "Element.h"
#include "Visitor.h"
#include <iostream>
using namespace std;

Element::Element() {
    // 构造函数
}

Element::~Element() {
    // 析构函数
}

void ConcreteElementA::Accept(Visitor* vis) {
    vis->VisitConcreteElementA(this);
    cout << "ConcreteElementA is being visited..." << endl;
}

void ConcreteElementB::Accept(Visitor* vis) {
    vis->VisitConcreteElementB(this);
    cout << "ConcreteElementB is being visited..." << endl;
}

代码片段 5:main.cpp

cpp 复制代码
// main.cpp
#include "Element.h"
#include "Visitor.h"
#include <iostream>
using namespace std;

int main(int argc, char* argv[]) {
    Visitor* visA = new ConcreteVisitorA();
    Visitor* visB = new ConcreteVisitorB();

    Element* elmA = new ConcreteElementA();
    Element* elmB = new ConcreteElementB();

    elmA->Accept(visA); // ConcreteVisitorA 访问 ConcreteElementA
    elmB->Accept(visA); // ConcreteVisitorA 访问 ConcreteElementB

    elmA->Accept(visB); // ConcreteVisitorB 访问 ConcreteElementA
    elmB->Accept(visB); // ConcreteVisitorB 访问 ConcreteElementB

    delete visA;
    delete visB;
    delete elmA;
    delete elmB;

    return 0;
}

代码说明

在实现 Visitor 模式时,需要注意以下几点:

(1)Visitor 类中的 Visit() 操作:

可以为 Element 类提供一个统一的 Visit() 接口,在 Accept() 中通过函数重载(overload)的方式调用具体的 Visit() 实现。

在 C++ 中,还可以通过 RTTI(运行时类型识别)来实现,即提供一个统一的 Visit() 函数体,传入 Element* 类型的参数,然后在函数内部通过 RTTI 决定具体的 ConcreteElement 类型,并执行相应的操作。

(2)双分派机制:

双分派机制使得 Accept() 操作能够根据 Element 和 Visitor 的类型动态决定调用哪个具体的 Visit() 实现。

四、总结讨论

Visitor 模式可以在不修改 `Element` 类的情况下为其增加新的操作,但这也带来了一些问题:

(1)破坏了封装性:

Visitor 模式要求 Visitor 可以从外部修改 Element 对象的状态,这通常通过以下两种方式实现:

Element 提供足够的 public 接口,使得 Visitor 可以通过调用这些接口修改 Element 的状态。

Element 暴露更多的细节给 Visitor,或者将 Visitor 声明为 Element 的 friend 类。这种方式会破坏封装性原则。

(2)扩展困难:

每增加一个 Element 的子类,就需要修改 Visitor 的接口,以提供对新子类的访问机制。无论是增加新的 Visit() 接口,还是修改 RTTI 实现,都会增加扩展的复杂性。

Visitor 模式是一种强大的设计模式,特别适用于需要在不修改现有类的情况下为其增加新操作的场景。然而,使用 Visitor 模式时需要权衡其带来的灵活性和对封装性的破坏,以及扩展时的复杂性。在实际开发中,应根据具体需求谨慎选择是否使用 Visitor 模式。

相关推荐
Dream it possible!32 分钟前
LeetCode 热题 100_字符串解码(71_394_中等_C++)(栈)
c++·算法·leetcode
My Li.1 小时前
c++的介绍
开发语言·c++
邪恶的贝利亚3 小时前
C++之序列容器(vector,list,dueqe)
开发语言·c++
原来是猿3 小时前
蓝桥备赛(13)- 链表和 list(上)
开发语言·数据结构·c++·算法·链表·list
成功助力英语中国话3 小时前
SDK编程,MFC编程,WTL编程之间的关系
c++·mfc
仟濹4 小时前
【算法 C/C++】二维差分
c语言·c++·算法
总斯霖5 小时前
题解:士兵排列
数据结构·c++·算法
稳兽龙5 小时前
P4268 [USACO18FEB] Directory Traversal G
c++·算法·换根dp
放氮气的蜗牛5 小时前
C++从入门到精通系列教程之第十篇:异常处理与调试技巧
开发语言·jvm·c++
LL5962145695 小时前
CEF在MFC上的示例工程
c++·mfc·cef