• 【Visitor模式】C++设计模式——访问器



        C++设计模式大全,23种设计模式合集详解—👉(点我跳转)

    一、设计流程探讨

      假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。图像中的每个节点既能代表复杂实体(例如一座城市),也能代表更精细的对象(例如工业区和旅游景点等)。如果节点代表的真实对象之间存在公路,那么这些节点就会相互连接。在程序内部,每个节点的类型都由其所属的类来表示,每个特定的节点则是一个对象。
    在这里插入图片描述
      一段时间后,你接到了实现将图像导出到 XML 文件中的任务。这些工作最初看上去非常简单。你计划为每个节点类添加导出函数,然后递归执行图像中每个节点的导出函数。解决方案简单且优雅:使用多态机制可以让导出方法的调用代码不会和具体的节点类相耦合。
      但你不太走运,系统架构师拒绝批准对已有节点类进行修改。他认为这些代码已经是产品了,不想冒险对其进行修改,因为修改可能会引入潜在的缺陷。
    在这里插入图片描述
      此外,他还质疑在节点类中包含导出 XML 文件的代码是否有意义。这些类的主要工作是处理地理数据。导出 XML 文件的代码放在这里并不合适。
      还有另一个原因,那就是在此项任务完成后,营销部门很有可能会要求程序提供导出其他类型文件的功能,或者提出其他奇怪的要求。这样你很可能会被迫再次修改这些重要但脆弱的类。
    解决方案:
      访问者模式建议将新行为放入一个名为访问者的独立类中,而不是试图将其整合到已有类中。现在,需要执行操作的原始对象将作为参数被传递给访问者中的方法,让方法能访问对象所包含的一切必要数据。
      如果现在该操作能在不同类的对象上执行会怎么样呢?比如在我们的示例中,各节点类导出 XML 文件的实际实现很可能会稍有不同。因此,访问者类可以定义一组(而不是一个)方法,且每个方法可接收不同类型的参数,如下所示:

    class ExportVisitor : public Visitor{
    	void doForCity(City c) { ... }
    	void doForIndustry(Industry f) { ... }
    	void doForSightSeeing(SightSeeing ss) { ... }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

      但我们究竟应该如何调用这些方法(尤其是在处理整个图像方面) 呢?这些方法的签名各不相同,因此我们不能使用多态机制。为了可以挑选出能够处理特定对象的访问者方法,我们需要对它的类进行检查。这是不是听上去像个噩梦呢?

    for (auto &node : graph) {
    	if (typeid(node) == typeid(City))
    		exportVisitor.doForCity(node);
    	if (typeid(node) == typeid(Industry))
    		exportVisitor.doForIndustry(node);
    	//...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      你可能会问,我们为什么不使用方法重载呢?就是使用相同的方法名称,但它们的参数不同。不幸的是,有些编程语言(例如 Java 和 C#) 支持重载也不行。由于我们无法提前知晓节点对象所属的类,所以重载机制无法执行正确的方法。方法会将 节点基类作为输入参数的默认类型。
      但是,访问者模式可以解决这个问题。它使用了一种名为双分派的技巧,不使用累赘的条件语句也可下执行正确的方法。与其让客户端来选择调用正确版本的方法,不如将选择权委派给作为参数传递给访问者的对象。由于该对象知晓其自身的类,因此能更自然地在访问者中选出正确的方法。它们会 “接收” 一个访问者并告诉其应执行的访问者方法。

    // 客户端代码
    for (auto &node : graph)
    	node.accept(exportVisitor);
    // 城市
    class City{
    	void accept(Visitor v){
    		v.doForCity(this);
    		//...
    	}
    };
    // 工业区
    class Industry{
    	void accept(Visitor v){
    		v.doForCity(this);
    		//...
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

      我承认最终还是修改了节点类,但毕竟改动很小,且使得我们能够在后续进一步添加行为时无需再次修改代码。
      现在,如果我们抽取出所有访问者的通用接口,所有已有的节点都能与我们在程序中引入的任何访问者交互。如果需要引入与节点相关的某个行为,你只需要实现一个新的访问者类即可。

    真实世界类比:
    在这里插入图片描述
      假如有这样一位非常希望赢得新客户的资深保险代理人。他可以拜访街区中的每栋楼,尝试向每个路人推销保险。所以,根据大楼内组织类型的不同,他可以提供专门的保单:
     ● 如果建筑是居民楼,他会推销医疗保险
     ● 如果建筑是银行,他会推销失窃保险
     ● 如果建筑是咖啡厅,他会推销火灾和洪水保险

    二、模式介绍

    (1)模式动机
      在软件构建过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法),如果直接在基类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有设计。
      如何在不更改类层次结构的前提下,在运行时根据需要透明地为类层次结构上的各个类动态添加新的操作,从而避免上述问题?
    (2)模式定义
      表示一个作用于某对象结构(如下代码的Element基类及其子类)中的各元素的操作。使得可以在不改变(稳定)各元素的类的前提下定义(扩展)作用于这些元素的新操作(变化)。
    (3)要点总结
    a). Visitor模式通过所谓双重分发(double dispatch)来实现在不更改(不添加新的操作-编译时)Element类层次结构的前提下,在运行时透明地为类层次结构上的各个类动态添加新的操作(支持变化)。
    b). 所谓双重分发即Visitor模式中间包括了两个多态分发(注意其中的多态机制):第一个为accept方法的多态辨析;第二个为visitElementX方法的多态辨析。
    c). Visitor模式的最大缺点在于扩展类层次结构(增添新的Element子类),会导致Visitor类的改变。因此Vistor模式适用于“Element类层次结构稳定,而其中的操作却经常面临频繁改动”。

    三、代码实现

      以下是不使用 Visitor 设计模式的伪代码,我们会发现这样会因变化而不仅需要修改基类,还需要修改各个子类。

    class Element{
    public:
    	virtual void Func1() = 0;
    	virtual void Func2(int data) = 0;
    	//...如果需要加Func3,不仅要修改基类,还要增加子类
    	virtual ~Element() {}
    };
    class ElementA : public Element{
    public:
    	void Func1() override { ... }
    	void Func2(int data) override { ... }
    };
    class ElementB : public Element{
    public:
    	void Func1() override { ... }
    	void Func2(int data) override { ... }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

      使用Visitor模式有个前提——能预料到未来可能会为如上代码Element类整个层次添加新的操作,但是不知需加多少操作、需加什么操作。但是这模式有个比较大的缺点,就如下类图结构,要求当你写 Visitor 的时候还要保证 Element类和子类 是稳定的,这是个非常大的前提,所以经常保证不了这一点。
    在这里插入图片描述

    class Element{
    public:
    	virtual void accept(Visitor &visitor) = 0;
    	virtual ~Element(){}
    };
    class ElementA : public Element{
    public:
    	void accept(Visitor &visitor) override {
    		visitor.visitElementA(*this);
    	}
    };
    class ElementB : public Element{
    public:
    	void accept(Visitor &visitor) override {
    		visitor.visitElementB(*this);
    	}
    };
    class Visitor{
    public:
    	virtual void visitElementA(ElementA *element) = 0;
    	virtual void visitElementB(ElementB *element) = 0;
    	virtual ~Visitor() {}
    };
    //=========  下面的内容是将来,即将来有新需求,我们就通过继承实现
    class Visitor1 : public Visitor{
    public:
    	void visitElementA(ElementA &element) override {
    		cout << "Visitor1 is processing ElementA" << endl;
    	}
    	void visitElementB(ElementB &element) override {
    		cout << "Visitor1 is processing ElementB" << endl;
    	}
    };
    class Visitor2 : public Visitor{
    public:
    	void visitElementA(ElementA &element) override {
    		cout << "Visitor2 is processing ElementA" << endl;
    	}
    	void visitElementB(ElementB &element) override {
    		cout << "Visitor2 is processing ElementB" << endl;
    	}
    };
    int main(){
    	Visitor2 visitor
    	ElementB element;
    	element.accept(visitor);		//这是 Visitor模式最重要的点,double dispatch
    	// 首先 accept 是虚函数,所以需找运行时类型即 ElementB
    	// 然后 ElementB 的 visitor.visitElementB() 我们传的是 visitor2
    	// 可以看出:accept 是第一次多态分发,visitElementB 是第二次多态分发
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
  • 相关阅读:
    学习记录609@python实现数据样本的过采样与欠采样
    在Python中什么是闭包?能做什么?
    C#委托的个人理解和体悟
    【洛谷题解/ZJOI2005】P2585 三色二叉树
    Makefile(make)之(3)输出变量值
    这世上又多了一只爬虫(spiderflow)
    错误记录-FileStream访问被拒绝
    阿里P8整合深入理解Dubbo实战+Kafka+分布式设计核心原理内部手册
    PyTorch安装步骤
    帧同步和状态同步
  • 原文地址:https://blog.csdn.net/u012011079/article/details/126280952