C++设计模式之行为型模式:访问者模式(Visitor)

访问者模式(Visitor)是行为型设计模式的一种,它允许你在不修改现有类的前提下,为类层次结构中的元素添加新的操作。这种模式通过将操作逻辑与元素类分离,实现了操作的灵活扩展,特别适合处理具有固定结构但需要频繁添加新操作的对象集合。

一、核心思想与角色

访问者模式的核心是"分离数据结构与操作",通过引入访问者对象封装对元素的操作,使操作可以独立于元素类变化。其核心角色如下:

角色名称 核心职责
抽象访问者(Visitor) 定义对每个具体元素类的访问接口,接口名与元素类对应(如visit(ElementA*))。
具体访问者(ConcreteVisitor) 实现抽象访问者接口,包含对每个元素的具体操作逻辑。
抽象元素(Element) 声明接受访问者的方法(accept(Visitor*)),该方法将访问者传入自身。
具体元素(ConcreteElement) 实现accept()方法,调用访问者对应的访问方法(如visitor->visit(this))。
对象结构(ObjectStructure) 存储元素集合,提供遍历元素的方法,通常会迭代调用元素的accept()方法。

核心思想 :元素通过accept()方法主动接受访问者的访问,访问者根据元素类型调用对应的visit()方法,从而实现对不同元素的差异化操作,且操作逻辑与元素类解耦。

二、实现示例(文档元素处理)

假设我们需要设计一个文档处理系统,文档中包含文本(Text)、图片(Image)和表格(Table)三种元素。系统需要支持多种操作:提取文本内容、计算元素数量、生成预览。使用访问者模式可在不修改元素类的情况下,灵活添加这些操作:

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <memory>

// 前向声明元素类
class Text;
class Image;
class Table;

// 1. 抽象访问者
class Visitor {
public:
    // 声明对每种元素的访问方法
    virtual void visit(Text* text) = 0;
    virtual void visit(Image* image) = 0;
    virtual void visit(Table* table) = 0;
    virtual ~Visitor() = default;
};

// 2. 抽象元素
class Element {
public:
    // 接受访问者访问
    virtual void accept(Visitor* visitor) = 0;
    virtual ~Element() = default;
};

// 3. 具体元素1:文本
class Text : public Element {
private:
    std::string content; // 文本内容

public:
    Text(const std::string& cnt) : content(cnt) {}

    // 获取文本内容(供访问者使用)
    std::string getContent() const { return content; }

    // 接受访问:调用访问者的对应方法
    void accept(Visitor* visitor) override {
        visitor->visit(this);
    }
};

// 3. 具体元素2:图片
class Image : public Element {
private:
    std::string url;    // 图片URL
    int width;          // 宽度
    int height;         // 高度

public:
    Image(const std::string& u, int w, int h) 
        : url(u), width(w), height(h) {}

    std::string getUrl() const { return url; }
    int getWidth() const { return width; }
    int getHeight() const { return height; }

    void accept(Visitor* visitor) override {
        visitor->visit(this);
    }
};

// 3. 具体元素3:表格
class Table : public Element {
private:
    int rows;   // 行数
    int cols;   // 列数

public:
    Table(int r, int c) : rows(r), cols(c) {}

    int getRows() const { return rows; }
    int getCols() const { return cols; }

    void accept(Visitor* visitor) override {
        visitor->visit(this);
    }
};

// 4. 具体访问者1:提取文本内容
class TextExtractionVisitor : public Visitor {
private:
    std::string extractedText;

public:
    void visit(Text* text) override {
        // 提取文本内容
        extractedText += text->getContent() + "\n";
    }

    void visit(Image* image) override {
        // 图片无文本,添加占位符
        extractedText += "[图片:" + image->getUrl() + "]\n";
    }

    void visit(Table* table) override {
        // 表格简单描述
        extractedText += "[表格:" + std::to_string(table->getRows()) 
                      + "行" + std::to_string(table->getCols()) + "列]\n";
    }

    // 获取提取的文本
    std::string getResult() const {
        return extractedText;
    }
};

// 4. 具体访问者2:计算元素数量
class CountingVisitor : public Visitor {
private:
    int textCount = 0;
    int imageCount = 0;
    int tableCount = 0;

public:
    void visit(Text* text) override {
        textCount++;
    }

    void visit(Image* image) override {
        imageCount++;
    }

    void visit(Table* table) override {
        tableCount++;
    }

    // 显示统计结果
    void showResult() const {
        std::cout << "元素统计:文本" << textCount 
                  << "个,图片" << imageCount 
                  << "个,表格" << tableCount << "个" << std::endl;
    }
};

// 4. 具体访问者3:生成预览
class PreviewVisitor : public Visitor {
public:
    void visit(Text* text) override {
        std::cout << "预览文本:" << (text->getContent().size() > 20 
            ? text->getContent().substr(0, 20) + "..." 
            : text->getContent()) << std::endl;
    }

    void visit(Image* image) override {
        std::cout << "预览图片:" << image->getUrl() 
                  << " (" << image->getWidth() << "x" << image->getHeight() << ")" << std::endl;
    }

    void visit(Table* table) override {
        std::cout << "预览表格:" << table->getRows() 
                  << "行" << table->getCols() << "列" << std::endl;
    }
};

// 5. 对象结构:文档(管理元素集合)
class Document {
private:
    std::vector<Element*> elements; // 元素列表

public:
    // 添加元素
    void addElement(Element* elem) {
        elements.push_back(elem);
    }

    // 接受访问者:遍历所有元素并调用accept()
    void accept(Visitor* visitor) {
        for (Element* elem : elements) {
            elem->accept(visitor);
        }
    }

    // 析构:释放元素
    ~Document() {
        for (Element* elem : elements) {
            delete elem;
        }
    }
};

// 客户端代码:使用文档处理系统
int main() {
    // 创建文档并添加元素
    Document* doc = new Document();
    doc->addElement(new Text("这是一段文档正文内容,用于演示访问者模式的使用。"));
    doc->addElement(new Image("logo.png", 200, 100));
    doc->addElement(new Table(5, 3));
    doc->addElement(new Text("这是文档的第二段文本,包含更多详细信息。"));
    doc->addElement(new Image("chart.png", 400, 300));

    // 1. 使用文本提取访问者
    std::cout << "=== 文本提取结果 ===" << std::endl;
    TextExtractionVisitor* textExtractor = new TextExtractionVisitor();
    doc->accept(textExtractor);
    std::cout << textExtractor->getResult() << std::endl;

    // 2. 使用计数访问者
    std::cout << "=== 元素统计结果 ===" << std::endl;
    CountingVisitor* counter = new CountingVisitor();
    doc->accept(counter);
    counter->showResult();
    std::cout << std::endl;

    // 3. 使用预览访问者
    std::cout << "=== 元素预览 ===" << std::endl;
    PreviewVisitor* previewer = new PreviewVisitor();
    doc->accept(previewer);

    // 释放资源
    delete previewer;
    delete counter;
    delete textExtractor;
    delete doc;

    return 0;
}

三、代码解析

  1. 抽象访问者(Visitor)

    定义了三个visit()方法,分别对应三种具体元素(TextImageTable),接口名与元素类型一一对应,确保访问者能处理所有元素。

  2. 抽象元素与具体元素

    • 抽象元素Element声明accept(Visitor*)方法,是元素接受访问的入口。
    • 具体元素(TextImageTable)实现accept()方法,通过调用visitor->visit(this)将自身传递给访问者,触发访问者的对应操作("双重分派"机制)。
    • 元素类封装自身数据(如Textcontent),并提供getter供访问者获取数据。
  3. 具体访问者

    每个访问者实现Visitor接口,针对不同元素提供差异化操作:

    • TextExtractionVisitor:提取文本内容,对图片和表格添加占位描述。
    • CountingVisitor:统计不同类型元素的数量。
    • PreviewVisitor:生成各种元素的预览信息。
      所有操作逻辑都封装在访问者中,元素类无需修改。
  4. 对象结构(Document)

    管理元素集合,提供addElement()添加元素和accept()方法遍历所有元素,调用其accept()方法接受访问者访问,简化了客户端对元素集合的操作。

  5. 客户端使用

    客户端创建文档、添加元素,然后创建不同访问者,通过文档的accept()方法让访问者处理所有元素,无需关心元素类型和操作细节。

四、核心优势与适用场景

优势
  1. 操作与元素解耦:新操作只需添加新访问者,无需修改元素类,符合开闭原则。
  2. 集中管理相关操作:同一类操作(如统计、预览)的逻辑集中在一个访问者中,便于维护。
  3. 灵活扩展:可在不影响元素类的前提下,为元素添加任意多的新操作。
  4. 多态处理:通过双重分派(元素类型和访问者类型)实现对不同元素的差异化处理。
适用场景
  1. 元素类结构稳定但操作多变:如文档编辑器(元素固定为文本、图片等,但需要频繁添加导出、统计等操作)。
  2. 需要对集合中不同类型元素执行不同操作:如报表系统中对表格、图表、文本的差异化处理。
  3. 避免在元素类中添加过多操作方法:当元素类不应包含与自身职责无关的操作时(如数据模型类不应包含UI渲染逻辑)。

五、局限性与与其他模式的区别

局限性
  1. 元素类扩展困难:若需新增元素类型(如文档中添加"图表"),所有访问者都需修改以支持新元素,违反开闭原则。
  2. 破坏封装性 :访问者需获取元素内部状态(通过getter),可能暴露元素的实现细节。
  3. 增加系统复杂度:引入多个访问者和元素类,使系统结构更复杂,理解成本提高。
与其他模式的区别
模式 核心差异点
访问者模式 分离元素与操作,通过访问者为元素添加新操作,强调"操作扩展"。
迭代器模式 提供遍历集合的统一接口,不关注元素操作,强调"遍历逻辑"。
策略模式 封装算法家族,客户端选择算法,不涉及元素类型差异处理。
模板方法 父类定义算法骨架,子类实现步骤,与元素-操作分离无关。

六、实践建议

  1. 适合稳定的元素结构:仅在元素类很少变化,而操作频繁变化时使用访问者模式。
  2. 结合组合模式:当元素具有树形结构(如文档包含章节,章节包含段落),可与组合模式结合,让访问者递归处理整个结构。
  3. 使用接口隔离访问:元素通过接口向访问者暴露必要数据,避免直接暴露私有成员,减少封装性破坏。
  4. 限制访问者数量:过多访问者会导致系统混乱,必要时可按功能划分访问者(如统计类、导出类)。

访问者模式的核心价值在于"在不修改元素的前提下扩展操作",它通过分离数据结构与操作逻辑,为具有固定结构的对象集合提供了灵活的功能扩展能力。在元素稳定而操作多变的场景中,访问者模式能有效降低代码耦合,提高系统的可维护性。

相关推荐
Lei活在当下3 小时前
【业务场景架构实战】8. 订单状态流转在 UI 端的呈现设计
android·设计模式·架构
Query*3 小时前
Java 设计模式——代理模式:从静态代理到 Spring AOP 最优实现
java·设计模式·代理模式
Q741_1474 小时前
C++ 面试基础考点 模拟题 力扣 38. 外观数列 题解 每日一题
c++·算法·leetcode·面试·模拟
L_09074 小时前
【Algorithm】二分查找算法
c++·算法·leetcode
祁同伟.5 小时前
【C++】多态
开发语言·c++
rechol5 小时前
C++ 继承笔记
java·c++·笔记
SunkingYang6 小时前
详细介绍C++中捕获异常类型的方式有哪些,分别用于哪些情形,哪些异常捕获可用于通过OLE操作excel异常
c++·excel·mfc·异常捕获·comerror
北冥湖畔的燕雀9 小时前
C++泛型编程(函数模板以及类模板)
开发语言·c++
Larry_Yanan13 小时前
QML学习笔记(四十二)QML的MessageDialog
c++·笔记·qt·学习·ui