当前位置: 首页 > article >正文

C++ 设计模式 - 访问者模式

一:概述

        访问者模式将作用于对象层次结构的操作封装为一个对象,并使其能够在不修改对象层次结构的情况下定义新的操作。

        《设计模式:可复用面向对象软件的基础》一书中的访问者模式因两个原因而具有传奇色彩:一是因为它的复杂性,二是因为它使用了一种名为“双重分派”的技术。双重分派指的是根据对象和函数参数选择成员函数的过程。当然,访问者模式的复杂性主要在于 C++ 本身不支持双重分派。在讨论单分派和双重分派之前,让我先谈谈访问者模式。

       什么是“双重分派”?双重分派(Double Dispatch)是一个动态选择调用方法的机制,其中被调用的具体函数依赖于两个对象的运行时类型,而不仅仅是一个对象的类型。这在多态编程中尤其有用,能够实现复杂的行为决策。在 C++ 中,双重分派通常通过虚函数和函数重载的组合实现,例如在访问者模式中。

二:使用场景

        1. 操作应该在对象层次结构上执行。

         2. 操作变更频繁。

         3. 对象层次结构是稳定的。 

三:类结构设计

        

  • Visitor 类:在对象结构上定义访问操作(visit 方法),用来描述对象结构中各元素可能执行的行为。

  • ConcreteVisitor 类:实现 Visitor 接口中的具体访问操作,为不同的元素提供特定的处理逻辑。

  • Element 类:表示对象结构中的元素,定义了 accept 方法,用于接收 Visitor 的操作请求。

  • ConcreteElement 类:具体的对象结构元素,实现 accept 方法,将自己作为参数传递给访问者,以完成双向的操作绑定。

四:一个具体的例子

        

      访问者模式有两种类型的层次结构:对象层次结构(CarElement)和操作层次结构(CarElementVisitor)。对象层次结构相对稳定,但操作层次结构可能需要支持新操作。CarElement 和 CarElementVisitor 都充当接口,这意味着每个具体的汽车元素(如 Wheel、Engine、Body 和 Car)都必须实现 accept(CarElementVisitor) 成员函数。相应地,每个具体的操作(如 CarElementDoVisitor 和 CarElementPrintVisitor)都必须实现四个重载的 visit(Wheel)visit(Engine)visit(Body)visit(Car) 方法。

      假设操作 CarElementPrintVisitor 被应用于对象层次结构。CarElementPrintVisitor 的任务可能是打印被访问的汽车零部件的名称。首先,像 Engine 这样的汽车元素接受访问者(accept(CarElementVisitor)),并使用访问者通过 visitor.visit(this) 调用操作层次结构,将自身作为参数传递。这确保了调用 CarElementPrintVisitor 上的 visit(Engine) 重载。访问 Car 是特殊的,因为 Car 由多个汽车元素组成。因此,Caraccept 成员函数将接受调用委托给它的所有汽车零部件。

     关于访问者的一个关键特征是:它依赖于两个对象,决定执行什么操作:访问者和被访问的对象。

五:代码示例

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

// 前向声明 Visitor 和元素类
class CarElementVisitor;

class CarElement {
public:
    // 接受访问者操作的接口,由具体元素实现
    virtual void accept(CarElementVisitor& visitor) const = 0;
    virtual ~CarElement() = default;
};

// 前向声明具体元素类
class Body;
class Car;
class Engine;
class Wheel;

// 访问者接口,定义了针对不同元素的访问操作
class CarElementVisitor {
public:
    // 针对不同具体元素的访问操作的虚函数
    virtual void visit(Body body) const = 0;
    virtual void visit(Car car) const = 0;
    virtual void visit(Engine engine) const = 0;
    virtual void visit(Wheel wheel) const = 0;
    virtual ~CarElementVisitor() = default;
};

// 轮胎类,表示汽车的一个轮胎
class Wheel : public CarElement {
public:
    // 构造函数,接收轮胎的名称
    Wheel(const std::string& n) : name(n) { }

    // 接受访问者,调用访问者的 visit(Wheel) 方法
    void accept(CarElementVisitor& visitor) const override {
        visitor.visit(*this);
    }

    // 获取轮胎的名称
    std::string getName() const {
        return name;
    }
private:
    std::string name; // 轮胎名称
};

// 车身类,表示汽车的车身
class Body : public CarElement {
public:
    // 接受访问者,调用访问者的 visit(Body) 方法
    void accept(CarElementVisitor& visitor) const override {
        visitor.visit(*this);
    }
};

// 发动机类,表示汽车的发动机
class Engine : public CarElement {
public:
    // 接受访问者,调用访问者的 visit(Engine) 方法
    void accept(CarElementVisitor& visitor) const override {
        visitor.visit(*this);
    }
};

// 汽车类,表示一辆汽车,由多个汽车元素组成
class Car : public CarElement {
public:
    // 构造函数,接收一个汽车元素列表
    Car(std::initializer_list<CarElement*> carElements) : elements{ carElements } {}

    // 接受访问者,依次让每个汽车元素接受访问
    void accept(CarElementVisitor& visitor) const override {
        for (auto elem : elements) {
            elem->accept(visitor); // 委托访问操作给子元素
        }
        visitor.visit(*this); // 最后访问汽车本身
    }
private:
    std::vector<CarElement*> elements; // 汽车元素列表
};

// 执行动作的访问者类,定义具体的访问行为
class CarElementDoVisitor : public CarE

六:相关设计模式 

  • 稳定的对象层次结构通常会应用 组合模式(Composite Pattern)
  • 迭代器模式(Iterator Pattern) 通常用于遍历对象层次结构。
  • 新的元素可以通过创建型模式(例如 工厂方法(Factory Method)原型模式(Prototype Pattern))来创建

七:优缺点

优点:

  • 可以轻松地向操作层次结构中添加新的操作(访问者)。
  • 一个操作可以被封装在一个访问者中。
  • 在遍历对象层次结构时,可以构建和维护状态。

缺点:

  • 如果需要在对象层次结构中添加新的被访问对象(VisitedObject),修改会非常困难。
  • 需要将新的被访问对象(VisitedObject)添加或从对象层次结构中移除。
  • 需要扩展访问者的接口,并在每个具体访问者中添加或移除 visit(VisitedObject) 成员函数。


八:参考:

1. The Visitor Pattern – MC++ BLOG
2. https://commons.wikimedia.org/w/index.php?curid=122709059


http://www.kler.cn/a/539716.html

相关文章:

  • 数组与指针1
  • JavaScript:还在用if判断属性是否存在?哒咩(?.)用起来
  • <论文>DeepSeek-R1:通过强化学习激励大语言模型的推理能力(深度思考)
  • UMLS初探
  • 排序合集(一)
  • xss闯关
  • MySQL数据库 - 阶段性体系总结
  • 【Vue3路由小技巧】用Hash模式打造流畅体验!
  • 【kafka系列】Topic 与 Partition
  • x小兔鲜vue.js
  • C#中的Frm_Welcome.Instance.Show(),是什么意思
  • c++ 输入输出笔记
  • 流氓软件一键屏蔽免疫工具Baidun Armor v3.2.1 绿色版
  • Centos Ollama + Deepseek-r1+Chatbox运行环境搭建
  • DeepSeek从入门到精通教程PDF清华大学出版
  • 【CubeMX+STM32】SD卡 文件系统读写 FatFs+SDIO+DMA
  • Java语言的区块链
  • kafka服务端之副本
  • Ubuntu 下 nginx-1.24.0 源码分析 - ngx_ssl_version 函数
  • 51单片机俄罗斯方块开机动画
  • 物理信息机器学习(PIML)的基础探讨及技术实现
  • 上传文件防木马函数
  • 【如何掌握CSP-J 信奥赛中的广搜算法】
  • 【每日一题 | 2025】2.3 ~ 2.9
  • Git 功能分支工作流程是如何支持社交化编程
  • 通过案例讲述docker,k8s,docker compose三者的关系