访问者模式(Vistor)

news2024/11/16 7:54:08

定义

访问者是一种行为设计模式,它能将算法与其所作用的对象隔离开来

前言

1. 问题

假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。图像中的每个节点既能代表复杂实体(例如一座城市), 也能代表更精细的对象(例如工业区和旅游景点等)。如果节点代表的真实对象之间存在公路,那么这些节点就会相互连接。在程序内部,每个节点的类型都由其所属的类来表示,每个特定的节点则是一个对象。

一段时间后, 你接到了实现将图像导出到 XML 文件中的任务。这些工作最初看上去非常简单。你计划为每个节点类添加导出函数,然后递归执行图像中每个节点的导出函数。解决方案简单且优雅:使用多态机制可以让导出方法的调用代码不会和具体的节点类相耦合。

但你不太走运,系统架构师拒绝批准对已有节点类进行修改。他认为这些代码已经是产品了,不想冒险对其进行修改,因1为修改可能会引入潜在的缺陷。

此外,他还质疑在节点类中包含导出 XML 文件的代码是否有意义。这些类的主要工作是处理地理数据。导出 XML 文件的代码放在这里并不合适。

还有另一个原因,那就是在此项任务完成后,营销部门很有可能会要求程序提供导出其他类型文件的功能,或者提出其他奇怪的要求。这样你很可能会被迫再次修改这些重要但脆弱的类。

2. 解决方案

访问者模式建议将新行为放入一个名为访问者的独立类中,而不是试图将其整合到已有类中。现在,需要执行操作的原始对象将作为参数被传递给访问者中的方法,让方法能访问对象所包含的一切必要数据。

使用了一种名为双分派的技巧,不使用累赘的条件语句也可下执行正确的方法。与其让客户端来选择调用正确版本的方法,不如将选择权委派给作为参数传递给访问者的对象。由于该对象知晓其自身的类,因此能更自然地在访问者中选出正确的方法。它们会“接收”一个访问者并告诉其应执行的访问者方法。

// 客户端代码
for (auto node : graph) {
    node->accpet(exportVistor);
}

// 城市
class City {
 public:
    void accept(Vistor *v) {
        v->doForCity(this);
    }
    // ...
};

// 工业区
class Industry {
 public:
    void accept(Vistor *v) {
        v->doForIndustry(this);
    }
    // ...
};

我承认最终还是修改了节点类,但毕竟改动很小,且使得我们能够在后续进一步添加行为时无需再次修改代码。

现在,如果我们抽取出所有访问者的通用接口,所有已有的节点都能与我们在程序中引入的任何访问者交互。如果需要引入与节点相关的某个行为,你只需要实现一个新的访问者类即可。

结构

  1. 访问者(Visitor)接口声明了一系列以对象结构的具体元素为参数的访问者方法。如果编程语言支持重载,这些方法的名称可以是相同的,但是其参数一定是不同的。
  2. 具体访问者(Concrete Visitor)会为不同的具体元素类实现相同行为的几个不同版本
  3. 元素(Element) 接口声明了一个方法来“接收” 访问者。该方法必须有一个参数被声明为访问者接口类型。
  4. 具体元素(Concrete Element)必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意,即使元素基类实现了该方法,所有子类都必须对其进行重写并调用访问者对象中的合适方法。
  5. 客户端(Client)通常会作为集合或其他复杂对象(例如一个组合树)的代表。 客户端通常不知晓所有的具体元素类,因为它们会通过抽象接口与集合中的对象进行交互。

适用场景

  • 如果你需要对一个复杂对象结构(例如对象树)中的所有元素执行某些操作,可使用访问者模式。

访问者模式通过在访问者对象中为多个目标类提供相同操作的变体,让你能在属于不同类的一组对象上执行同一操作。

  • 可使用访问者模式来清理辅助行为的业务逻辑。

该模式会将所有非主要的行为抽取到一组访问者类中,使得程序的主要类能更专注于主要的工作。

  • 当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可使用该模式。

你可将该行为抽取到单独的访问者类中,只需实现接收相关类的对象作为参数的访问者方法并将其他方法留空即可。

实现方式

  1. 在访问者接口中声明一组“访问”方法,分别对应程序中的每个具体元素类。
  2. 声明元素接口。如果程序中已有元素类层次接口,可在层次结构基类中添加抽象的“接收”方法。该方法必须接受访问者对象作为参数。
  3. 在所有具体元素类中实现接收方法。这些方法必须将调用重定向到当前元素对应的访问者对象中的访问者方法上。
  4. 元素类只能通过访问者接口与访问者进行交互。不过访问者必须知晓所有的具体元素类,因为这些类在访问者方法中都被作为参数类型引用。
  5. 为每个无法在元素层次结构中实现的行为创建一个具体访问者类并实现所有的访问者方法。你可能会遇到访问者需要访问元素类的部分私有成员变量的情况。在这种情况下,你要么将这些变量或方法设为公有,这将破坏元素的封装;要么将访问者类嵌入到元素类中。后一种方式只有在支持嵌套类的编程语言中才可能实现。
  6. 客户端必须创建访问者对象并通过“接收”方法将其传递给元素。

优点

  • 开闭原则。你可以引入在不同类对象上执行的新行为,且无需对这些类做出修改。
  • 单一职责原则。可将同一行为的不同版本移到同一个类中。
  • 访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构(例如对象树),并在结构中的每个对象上应用访问者时,这些信息可能会有所帮助。

缺点

  • 每次在元素层次结构中添加或移除一个类时,你都要更新所有的访问者。
  • 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限。

Visitor.hpp

#ifndef VISTOR_H_
#define VISTOR_H_

#include <string>

class Apple;
class Book;

// 抽象访问者
class Vistor {
 public:
    void set_name(std::string name) {
        name_ = name;
    }

    virtual void visit(Apple *apple) = 0;
    virtual void visit(Book *book) = 0;

 protected:
    std::string name_;
};

#endif  // VISTOR_H_

ConcreteVisitor.hpp

#ifndef CONCRETE_VISTOR_H_
#define CONCRETE_VISTOR_H_

#include <iostream>
#include "Visitor.hpp"

// 具体访问者类: 顾客
class Customer : public Vistor {
 public:
    void visit(Apple *apple) {
        std::cout << "顾客" << name_ << "挑选苹果。" << std::endl;
    }

    void visit(Book *book) {
        std::cout << "顾客" << name_ << "买书。" << std::endl;
    }
};

// 具体访问者类: 收银员
class Saler : public Vistor {
 public:
    void visit(Apple *apple) {
        std::cout << "收银员" << name_ << "给苹果过称, 然后计算价格。" << std::endl;
    }

    void visit(Book *book) {
        std::cout << "收银员" << name_ << "计算书的价格。" << std::endl;
    }
};

#endif  // CONCRETE_VISTOR_H_

Element.hpp

#ifndef ELEMENT_H_
#define ELEMENT_H_

#include "Visitor.hpp"

// 抽象元素类
class Product {
 public:
    virtual void accept(Vistor *vistor) = 0;
};

#endif  // ELEMENT_H_

ConcreteElement.hpp

#ifndef CONCRETE_ELEMENT_H_
#define CONCRETE_ELEMENT_H_

#include "Element.hpp"

// 具体产品类: 苹果
class Apple : public Product {
 public:
    void accept(Vistor *vistor) override {
        vistor->visit(this);
    }
};

// 具体产品类: 书籍
class Book : public Product {
 public:
    void accept(Vistor *vistor) override {
        vistor->visit(this);
    }
};


#endif  // CONCRETE_ELEMENT_H_

Client.hpp

#ifndef CLIENT_H_
#define CLIENT_H_

#include <list>
#include "Visitor.hpp"
#include "Element.hpp"

// 购物车
class ShoppingCart {
 public:
    void accept(Vistor *vistor) {
        for (auto prd : prd_list_) {
            prd->accept(vistor);
        }
    }

    void addProduct(Product *product) {
        prd_list_.push_back(product);
    }

    void removeProduct(Product *product) {
        prd_list_.remove(product);
    }

 private:
    std::list<Product*> prd_list_;
};

#endif  // CLIENT_H_

main.cpp

#include "Client.hpp"
#include "ConcreteElement.hpp"
#include "ConcreteVisitor.hpp"

int main() {
    Book book;
    Apple apple;
    ShoppingCart basket;

    basket.addProduct(&book);
    basket.addProduct(&apple);

    Customer customer;
    customer.set_name("小张");
    basket.accept(&customer);

    Saler saler;
    saler.set_name("小杨");
    basket.accept(&saler);

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/707830.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Nginx【Docker(安装Nginx、Nginx服务启停控制、全局块、events块、HTTP块)】(二)-全面详解(学习总结---从入门到深化)

目录 Docker安装Nginx Nginx服务启停控制 Nginx配置指令详解_全局块 Nginx配置指令详解_events块 Nginx配置指令详解_HTTP块 Docker安装Nginx 拉取官方的Nginx镜像 [rootlocalhost ~]# docker pull nginx 以下命令使用 Nginx 默认的配置来启动一个 Nginx 容器实例&#xf…

小驰私房菜_28_Qcom Camx相关名词

(Qcom 7325平台) CSID = Camera Serial Interface Decoder module IPE = Image Processing Engine IFE (x3) = Image Front End IFE_lite (x2) BPS = Bayer processing segment (for Snapshot) IPE = Image Processing Engine VPU = Video Processing Unit (CODEC) DP…

matplotlib布局模式

栅格布局 import matplotlib.pyplot as plt import numpy as np plt.figure("OBJ")x np.linspace(-np.pi, np.pi, 1000) cosy np.cos(x) siny np.sin(x) y x * 0.5 timesy x ** 2 # 创建九宫格 gs plt.GridSpec(3, 3) # 第0-1行&#xff0c;第2列 plt.subplot…

Eclipse中有用的快捷键

Eclipse中有的快捷键自己记不清楚&#xff0c;但用起来又很方便&#xff0c;遇到了就放在这边备忘。 【CtrlO】快速定位某个类中的属性、方法 有时候&#xff0c;一个类中的属性、方法比较多&#xff0c;想用快捷键快速查找&#xff0c;提升效率。 举例&#xff1a;我想查找…

MYSQL-聚合函数及分组查询

常用聚合函数 COUNT() 求有多少行 SUM() 求和 AVG() 求平均值 MIN() 求最小值 MAX() 求最大值 举个栗子 SELECT AVG(price) FROM products WHERE price_id > 10; 这行代码就是在求id大于10的价格的平均值 AVG(price)表示求price列的平均值 执行逻辑为 先由WHERE…

Mock在接口测试中的实际应用

关于Mock测试 01、含义和目的 1、 什么是mock测试&#xff1f; Mock 测试就是在测试过程中&#xff0c;对于某些不容易构造&#xff08;如 HttpServletRequest 必须在Servlet 容器中才能构造出来&#xff09;或者不容易获取的比较复杂的对象&#xff08;如 JDBC 中的ResultSe…

chatgpt赋能python:下载完Python,如何进入编辑器

下载完Python&#xff0c;如何进入编辑器 Python是一门高级编程语言&#xff0c;具有简单易懂、易于学习、可拓展性强等特点&#xff0c;被广泛应用于Web应用、桌面应用、科学计算、人工智能等众多领域。如果你已经下载并安装了Python&#xff0c;那么接下来如何进入编辑器呢&…

uniapp智慧停车场系统微信小程序h5、APP源码 智能停车系统源码 安装搭建部署教程

【APP】: flutter(原生混合框架&#xff0c;不是web封装&#xff0c;原生应用&#xff0c;一套代码直接生成原生Android和ios应用)&#xff0c;既不损失性能&#xff0c;也能降低开发成本 【小程序/h5/公众号】&#xff1a;uni-app(底层框架Vue) 【后台管理】&#xff1a;vue-e…

DeepSpeed-Chat 打造类ChatGPT全流程 笔记一

这篇文章主要是对DeepSpeed Chat的功能做了一些了解&#xff0c;然后翻译了几个主要的教程了解了一些使用细节。最后在手动复现opt-13b做actor模型&#xff0c;opt-350m做reward模型进行的chatbot全流程训练时&#xff0c;踩了一些坑也分享出来了。最后使用训练后的模型做servi…

计算机组成原理(课堂测验3次)

3、同步通信与异步通信的主要区别是什么&#xff0c;说明通信双方如何联络。 同步通信和异步通信的主要区别是&#xff1a;前者有公共时钟线&#xff0c;所有设备按统一的时序、同一的传输周期进行信息传输&#xff0c;通信双方按约定好的时序联络&#xff1b;后者没有公共时钟…

探秘直链网盘:高效传输、便捷分享的存储利器!

什么是直链网盘&#xff1f; 直链网盘是一种用于存储和共享文件的在线服务。它为用户提供了一个方便的方式来存储和访问他们的文件&#xff0c;而无需依赖本地存储设备。直链网盘的主要特点是它们可以生成直接下载链接&#xff0c;允许用户快速下载文件&#xff0c;而不需要进…

使用 Sigstore 签名的 Elastic Stack 容器镜像!

作者&#xff1a;Maxime Greau 软件供应链攻击不断增加。 这就是为什么这个主题是安全领导者的首要任务。 在这方面&#xff0c;这篇博文重点介绍了使用 Sigstore 对 Elastic Stack 容器镜像进行签名的新功能&#xff0c;以便&#xff1a; 保护 Elastic 软件供应链工作流程为…

java面试Day14

1.如何使用 Redis 实现一个排行榜&#xff1f; Redis实现排行榜是Redis中一个很常见的场景&#xff0c;主要使用的是ZSet进行实现&#xff0c;下面是为什么选用ZSet&#xff1a; 有序性&#xff1a;排行榜肯定需要实现一个排序的功能&#xff0c;在Redis中有序的数据结构有List…

Tauri:跨平台探索之旅

一、简介 Tauri 是一个跨平台 GUI 框架&#xff0c;与 Electron 的思想基本类似。都是属于跨平台技术的解决方案 优缺点快速分析 我们一般会把tauri作为 Electron 的替代方案&#xff0c;electron优点咱们不看&#xff0c;这里就提两个electron比较明显的问题&#xff1a; 安装…

高考志愿填报的个人看法,希望能对你有所启发

各省高考成绩已出&#xff0c;又到一年高考季。张雪峰提到&#xff1a;“普通家庭不要光谈理想&#xff0c;也要谈落地。”志愿怎样填报、选专业还是选学校、什么专业好就业、高考志愿主要看什么&#xff1f; 作为一名过来人&#xff0c;今天就站在小部分群体的角度来聊聊&…

自动化测试常见的三大问题及解决方案

各位小伙伴们&#xff0c;大家好&#xff0c;今天给大家带来的是关于自动化测试常见的三大问题及解决方案&#xff0c;希望给遇到这三大问题的你一些帮助 一&#xff0c;就是我们定位元素的时候&#xff0c;定位不到或有时定位得到&#xff0c;有时定位不到。 特别是喜欢复制…

策略模式(Strategy)

定义 策略是一种行为设计模式&#xff0c;它能让你定义一系列算法&#xff0c;并将每种算法分别放入独立的类中&#xff0c;以使算法的对象能够相互替换。 前言 1. 问题 你打算为游客们创建一款导游程序。该程序的核心功能是提供美观的地图&#xff0c;以帮助用户在任何城市…

数据结构 手撕顺序表(动态版)+代码详解

⭐️ 顺序表介绍 顺序表是线性表的一种。 &#x1f320;什么是线性表呢&#xff1f; 线性表是数据结构的一种&#xff0c;一个线性表是 n n n个具有相同特性的数据元素的有限序列。常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串… &#x1f320;什么是顺序表呢&…

Docker + Wasm = 王炸!!!

Docker 宣布推出与 WebAssembly 集成 (DockerWasm) 的首个技术预览版&#xff0c;并表示公司已加入字节码联盟 (Bytecode Alliance)&#xff0c;成为投票成员。 Bytecode Alliance&#xff08;字节码联盟&#xff09;由 Mozilla、Fastly、Intel 与 Red Hat 联合成立&#xff0c…

Redis的优化(二)

Redis的高可用 一、主从复制优化主从复制的作用主从复制流程主从复制实验 二、Redis 哨兵模式哨兵模式的作用故障转移机制主节点的选举原则哨兵模式的实验 三、Redis群集模式集群的作用Redis集群的数据分片搭建Redis群集模式实验 ●主从复制&#xff1a;主从复制是高可用Redis的…