访问者模式(Visitor Pattern)是一种行为型设计模式,它允许你在不修改现有类的情况下,为类层次结构中的元素添加新的操作。这种模式通过将操作封装在访问者对象中,实现了数据结构与操作的分离,使得操作可以独立于元素结构而变化。

访问者模式的核心角色

  1. 访问者(Visitor):定义一个访问每个元素的接口,为每个具体元素类对应一个访问操作
  2. 具体访问者(Concrete Visitor):实现访问者接口,定义对每个元素的具体操作
  3. 元素(Element):定义一个接受访问者的接口,通常是accept()方法
  4. 具体元素(Concrete Element):实现元素接口,通过调用访问者的对应方法来接受访问
  5. 对象结构(Object Structure):持有元素集合,提供遍历元素的方法,允许访问者访问其元素

访问者模式的实现示例

下面以"文档编辑器"为例展示访问者模式的实现,文档中包含文本、图片和表格等元素,我们可以通过不同的访问者实现不同的操作(如导出为HTML、统计字数等):

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

// 前向声明
class TextElement;
class ImageElement;
class TableElement;

// 访问者接口
class Visitor {
public:
    virtual void visit(TextElement* element) = 0;
    virtual void visit(ImageElement* element) = 0;
    virtual void visit(TableElement* element) = 0;
    virtual ~Visitor() = default;
};

// 元素接口
class Element {
public:
    virtual void accept(Visitor* visitor) = 0;
    virtual ~Element() = default;
};

// 具体元素:文本
class TextElement : public Element {
private:
    std::string content;
    int wordCount;

public:
    TextElement(const std::string& text, int words) 
        : content(text), wordCount(words) {}

    std::string getContent() const { return content; }
    int getWordCount() const { return wordCount; }

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

// 具体元素:图片
class ImageElement : public Element {
private:
    std::string filename;
    int width;
    int height;

public:
    ImageElement(const std::string& name, int w, int h)
        : filename(name), width(w), height(h) {}

    std::string getFilename() const { return filename; }
    int getWidth() const { return width; }
    int getHeight() const { return height; }

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

// 具体元素:表格
class TableElement : public Element {
private:
    std::string title;
    int rows;
    int cols;

public:
    TableElement(const std::string& t, int r, int c)
        : title(t), rows(r), cols(c) {}

    std::string getTitle() const { return title; }
    int getRows() const { return rows; }
    int getCols() const { return cols; }

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

// 具体访问者:HTML导出器
class HtmlExporter : public Visitor {
public:
    void visit(TextElement* element) override {
        std::cout << "<p>" << element->getContent() << "</p>" << std::endl;
    }

    void visit(ImageElement* element) override {
        std::cout << "<img src=\"" << element->getFilename() 
                  << "\" width=\"" << element->getWidth()
                  << "\" height=\"" << element->getHeight() << "\">" << std::endl;
    }

    void visit(TableElement* element) override {
        std::cout << "<table title=\"" << element->getTitle() << "\">" << std::endl;
        std::cout << "  <!-- 表格内容,共 " << element->getRows() 
                  << " 行 " << element->getCols() << " 列 -->" << std::endl;
        std::cout << "</table>" << std::endl;
    }
};

// 具体访问者:统计器
class StatisticsVisitor : public Visitor {
private:
    int totalWords;
    int imageCount;
    int tableCount;
    int totalCells;

public:
    StatisticsVisitor() 
        : totalWords(0), imageCount(0), tableCount(0), totalCells(0) {}

    void visit(TextElement* element) override {
        totalWords += element->getWordCount();
    }

    void visit(ImageElement* element) override {
        imageCount++;
    }

    void visit(TableElement* element) override {
        tableCount++;
        totalCells += element->getRows() * element->getCols();
    }

    void showStatistics() const {
        std::cout << "\n=== 文档统计信息 ===" << std::endl;
        std::cout << "总字数: " << totalWords << std::endl;
        std::cout << "图片数量: " << imageCount << std::endl;
        std::cout << "表格数量: " << tableCount << std::endl;
        std::cout << "表格总单元格数: " << totalCells << std::endl;
    }
};

// 对象结构:文档
class Document {
private:
    std::vector<std::unique_ptr<Element>> elements;

public:
    void addElement(std::unique_ptr<Element> element) {
        elements.push_back(std::move(element));
    }

    // 接受访问者,遍历所有元素
    void accept(Visitor* visitor) {
        for (const auto& element : elements) {
            element->accept(visitor);
        }
    }
};

// 客户端使用
int main() {
    // 创建文档并添加元素
    Document doc;
    doc.addElement(std::make_unique<TextElement>("这是一段示例文本", 10));
    doc.addElement(std::make_unique<ImageElement>("photo.jpg", 800, 600));
    doc.addElement(std::make_unique<TableElement>("数据表格", 5, 3));
    doc.addElement(std::make_unique<TextElement>("这是另一段文本", 8));

    // 使用HTML导出器访问文档
    std::cout << "=== HTML导出结果 ===" << std::endl;
    HtmlExporter htmlExporter;
    doc.accept(&htmlExporter);

    // 使用统计器访问文档
    StatisticsVisitor statsVisitor;
    doc.accept(&statsVisitor);
    statsVisitor.showStatistics();

    return 0;
}

访问者模式的工作原理

  1. 元素类提供accept()方法,该方法接收一个访问者对象并调用访问者的对应visit()方法
  2. 每个具体元素的accept()方法会调用访问者中与自己类型匹配的visit()方法
  3. 访问者通过visit()方法的参数获取元素的具体类型,并对元素执行相应操作
  4. 对象结构(如文档)负责管理元素集合,并提供方法让访问者可以遍历所有元素

访问者模式的双重分派

访问者模式通过"双重分派"机制实现了操作与元素类型的绑定:

  • 第一次分派:客户端调用元素的accept()方法
  • 第二次分派:元素的accept()方法调用访问者的对应visit()方法

这种机制使得访问者可以根据元素的具体类型执行不同的操作,而无需使用大量的条件判断。

访问者模式的应用场景

  1. 当需要为一个包含多个不同类型元素的对象结构添加新操作时
  2. 当对象结构相对稳定,但需要经常为其添加新操作时
  3. 当需要在不修改元素类的前提下,对元素进行多种不同且不相关的操作时

访问者模式的优缺点

优点

  • 分离了数据结构与操作,使操作可以独立演化
  • 便于添加新的操作,只需新增访问者类,符合开放-封闭原则
  • 将相关操作集中到一个访问者类中,而不是分散在各个元素类中
  • 可以在访问过程中收集元素的信息(如统计器示例)

缺点

  • 增加了新元素类型的难度,需要修改所有访问者类
  • 破坏了元素的封装性,访问者需要了解元素的内部细节才能执行操作
  • 如果对象结构经常变化(添加新元素),访问者模式会变得难以维护

访问者模式在C++的STL中也有间接应用,例如std::for_each算法可以看作是一种简化的访问者模式。在实际开发中,访问者模式常用于编译器的语法树分析、文档解析、复杂集合的多种遍历操作等场景。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐