基于C++的《Head First设计模式》笔记——访问者模式

目录

一.专栏介绍

二.传统继承多态例子与代码

三.访问者模式概念

四.用访问者模式改写案例

五.案例总结

六.访问者模式优点

七.访问者模式缺点


一.专栏介绍

本专栏是我学习《head first》设计模式的笔记。这本书中是用Java语言为基础的,我将用C++语言重写一遍,并且详细讲述其中的设计模式,涉及是什么,为什么,怎么做,自己的心得等等。希望阅读者在读完我的这个专题后,也能在开发中灵活且正确的使用,或者在面对面试官时,能够自信地说自己熟悉常用设计模式。

本章将开始**访问者模式(Visitor Pattern)**的学习。

二.传统继承多态例子与代码

我以电脑的硬件为例子来编写代码。首先硬件为基类,然后具体的显卡和声卡为子类,基类提供硬件驱动版本检查方法和功能状态检查方法(都是纯虚函数),子类重写它们。

代码如下:

cpp 复制代码
// 电脑硬件基类(抽象类)
class ComputerPart
{
public:
	ComputerPart(const string& name, size_t version, bool isNormal) :
		_name(name), _version(version), _isNormal(isNormal)
	{}
	virtual ~ComputerPart() = default;
	const string& getName() { return _name; }

	// 驱动版本检查方法
	virtual void checkVersion() = 0;
	// 功能状态检查方法
	virtual void checkFunction() = 0;

protected:
	string _name;			// 硬件名称
	size_t _version;		// 版本号
	bool _isNormal;			// 功能是否正常标志
};

// GPU硬件子类
class GpuPart : public ComputerPart
{
public:
	GpuPart(const string& name, size_t version, bool isNormal) :
		ComputerPart(name, version, isNormal)
	{}

	void checkVersion() override
	{
		if(_version >= 500)
			cout << "GPU驱动[" << _name << "] " << " : 版本正常" << endl;
		else
			cout << "GPU驱动[" << _name << "] " << " : 版本较低" << endl;
	}
	void checkFunction() override
	{
		if (_version)
			cout << "显卡[" << _name << "]" << " : 功能正常" << endl;
		else
			cout << "显卡[" << _name << "]" << " : 功能异常" << endl;
	}
};

// 声卡硬件子类
class SoundCardPart : public ComputerPart
{
public:
	SoundCardPart(const string& name, size_t version, bool isNormal) :
		ComputerPart(name, version, isNormal)
	{}

	void checkVersion() override
	{
		if (_version >= 500)
			cout << "声卡驱动[" << _name << "] " << " : 版本正常" << endl;
		else
			cout << "声卡驱动[" << _name << "] " << " : 版本较低" << endl;
	}
	void checkFunction() override
	{
		if (_version)
			cout << "声卡[" << _name << "]" << " : 功能正常" << endl;
		else
			cout << "声卡[" << _name << "]" << " : 功能异常" << endl;
	}
};

这就是传统继承 + 多态的写法,它的数据 + 逻辑耦合在一起,也就是硬件类(GPU、声卡)自己包含检测逻辑。

具体表现就是每加一个功能,就要修改所有类。比如加一个checkTemperature()温度检测:

  • 要改基类ComputerPart

  • 要改子类GpuPart

  • 要改SoundCardPart

以后要是再有更多的硬件,比如CPU类,内存条类,那么对应的检测功能函数都要一起添加。

这样就违反了开闭原则 。(对扩展开放,对修改关闭)。我们在这样的代码中加功能必须修改旧代码,导致风险高、容易出bug、维护难

下面有请访问者模式来改变这个现状。

三.访问者模式概念

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

模式角色:

元素基类(element):定义接收访问者的接口accept()

具体元素:实现accept(),让访问者访问自己

访问者基类(Visitor):定义对每种元素的访问接口

具体访问者:实现具体的"访问/检测"逻辑

对象结构:管理一组部件,统一接受访问者检测

元素类的accept()需要访问者的基类引用或者指针作为参数 ,访问者类的访问元素类的函数也需要元素类的基类引用或者指针作为参数

为什么是彼此的指针或者引用?

  1. 为了多态
  2. 为了避免循环引用

四.用访问者模式改写案例

直接展示代码和注释讲解。

visitorPattern.h:

cpp 复制代码
#pragma once
#include <string>
#include <vector>
#include <iostream>
using namespace std;

// 访问者基类的前置声明,避免循环引用手段之一
class VisitorBase;

// 元素类(电脑硬件)的基类,仅包含自己的属性(成员变量),相应的get函数,还有访问者的入口:accept函数
class ComputerPart
{
public:
	ComputerPart(const string& name, size_t version, bool isNormal):
		_name(name), _version(version), _isNormal(isNormal)
	{}
	virtual ~ComputerPart() = default;
	const string& getName() { return _name; }

	size_t getVersion() { return _version; }
	bool isNormal() { return _isNormal; }

	// 用访问者基类的指针作为参数,一个是为了多态,一个是避免循环引用手段之二
	virtual void accept(VisitorBase* visitor) = 0;
protected:
	string _name;			// 硬件名字
	size_t _version;		// 硬件版本
	bool _isNormal;			// 功能状态标记
};

// 访问者基类,提供访问各个具体硬件的接口
class VisitorBase
{
public:
	VisitorBase(const string& name) :
		_name(name)
	{}
	virtual ~VisitorBase() = default;
	const string& getName() { return _name; }
	// 访问GPU硬件的接口,用硬件基类的指针作为参数,一个是为了多态,一个是避免循环引用手段之二
	virtual void visitCheckGpu(ComputerPart* part) = 0;
	// 访问声卡硬件的接口,同理
	virtual void visitCheckSoundCard(ComputerPart* part) = 0;
protected:
	string _name;				// 访问者名字
};

// GPU子类
class GpuPart : public ComputerPart
{
public:
	GpuPart(const string& name, size_t version, bool isNormal):
		ComputerPart(name, version, isNormal)
	{}
	// 实现accept函数,让访问者访问自己
	void accept(VisitorBase* visitor) override
	{
		visitor->visitCheckGpu(this);
	}
};

// 声卡子类
class SoundCardPart : public ComputerPart
{
public:
	SoundCardPart(const string& name, size_t version, bool isNormal):
		ComputerPart(name, version, isNormal)
	{}
	// 实现accept函数,让访问者访问自己
	void accept(VisitorBase* visitor) override
	{
		visitor->visitCheckSoundCard(this);
	}
};

// 驱动版本检测访问者
class DriverCheckVisitor : public VisitorBase
{
public:
	DriverCheckVisitor(const string& name):
		VisitorBase(name)
	{}
	// 实现visitCheckGpu方法,根据电脑硬件基类指针获取硬件的状态来检测
	void visitCheckGpu(ComputerPart* part) override
	{
		if (part->getVersion() >= 500)
			cout << "显卡驱动[" << part->getName() << "]" << " : 版本正常" << endl;
		else 
			cout << "显卡驱动[" << part->getName() << "]" << " : 版本较低" << endl;
	}
	// 实现visitCheckSoundCard方法,同理
	void visitCheckSoundCard(ComputerPart* part) override
	{
		if (part->getVersion() >= 333)
			cout << "声卡驱动[" << part->getName() << "]" << " : 版本正常" << endl;
		else
			cout << "声卡驱动[" << part->getName() << "]" << " : 版本较低" << endl;
	}
};

// 功能状态检测访问者
class FunctionVisitor : public VisitorBase
{
public:
	FunctionVisitor(const string& name) :
		VisitorBase(name)
	{}
	// 同理
	void visitCheckGpu(ComputerPart* part) override
	{
		if (part->isNormal())
			cout << "显卡[" << part->getName() << "]" << " : 功能正常" << endl;
		else
			cout << "显卡[" << part->getName() << "]" << " : 功能异常" << endl;
	}
	// 同理
	void visitCheckSoundCard(ComputerPart* part) override
	{
		if (part->isNormal())
			cout << "声卡[" << part->getName() << "]" << " : 功能正常" << endl;
		else
			cout << "声卡[" << part->getName() << "]" << " : 功能异常" << endl;
	}
};

// 对象结构 管理类,所谓增删查改硬件,并提供以访问者为参数
// 检测所有零件的方法:checkParts(VisitorBase* visitor)
class PartManager
{
public:
	PartManager() = default;
	void addPart(ComputerPart* part)
	{
		if (part) parts.push_back(part);
	}
	void erase(ComputerPart* part)
	{
		for (auto it = parts.begin();it != parts.end();++it)
		{
			if (*it == part) parts.erase(it);
		}
	}
	void checkParts(VisitorBase* visitor)
	{
		if (visitor) cout << "[" << visitor->getName() << "] 开始检测工作:" << endl;
		for (const auto& part : parts)
		{
			part->accept(visitor);	// 决定具体调用哪个访问类(visitor)里的哪个访问(visit)方法依靠
								    // 两个对象:part和visitor,这就叫双分派
		}
	}
private:
	vector<ComputerPart*> parts;
};

这个双分派特殊说明一下,某些小众语言支持所谓的单分派,也就是一个对象就可以决定具体调用哪个访问类(visitor)里的哪个访问(visit)方法,我们C++不管那些。

在具体实现的visit方法里,我们可以使用dynamic_castComputerPart* 强转为具体的硬件类型GpuPart* ,然后调用GpuPart类里面的独有方法,比如给它一个独有的getPower()函数,用来获取Gpu的算力。dynamic_cast就是C++中所谓的RTTI,用于基类转子类,子类也可以转基类。 关于RTTI,后期会出博客具体讲解。

main.cpp:

cpp 复制代码
#include "visitorPattern.h"

int main()
{
	// 硬件管理对象
	PartManager partManager;
	// 两个访问者对象
	VisitorBase* driverCheckVisitor = new DriverCheckVisitor("驱动版本检测工程师");
	VisitorBase* functionVisitor = new FunctionVisitor("部件功能检测工程师");

	// 具体硬件对象
	ComputerPart* gpu = new GpuPart("5090", 888, true);
	ComputerPart* soundCard = new SoundCardPart("超级声卡", 777, false);

	// 向硬件管理类里添加硬件
	partManager.addPart(gpu);
	partManager.addPart(soundCard);

	// 检测所有硬件的驱动版本
	partManager.checkParts(driverCheckVisitor);
	cout << endl;
	// 检测所有硬件的功能状态
	partManager.checkParts(functionVisitor);

	delete driverCheckVisitor;
	delete functionVisitor;
	delete gpu;
	delete soundCard;

	return 0;
}

运行结果:

访问者模式改写后的特点:

  1. 元素类(硬件类)只存数据,不写功能逻辑,附和单一职责原则
  2. 所有检测逻辑都放在访问者里,附和单一职责原则
  3. 加新功能,只需要加新访问者,完全不改旧类,附和开闭原则
  4. 元素类封装性不重要,因为需要提供get方法给visitor访问
  5. 元素类不容易变(比如电脑的硬件类型是固定的那几个,CPU,GPU,内存等等)

比如,加个温度检测功能,那就加一个TemperatureVisitor类,继承VisitorBase类,重写visitCheckGpu(ComputerPart* part)和visitCheckSoundCard(ComputerPart* part)方法,调用ComputerPart里的getTemperature()方法(默认有)来进行检测判断。完全不用改元素类(GPU、声卡、硬件基类),代码安全、易扩展、易维护。

又比如,假如发明了一个YPU这个硬件,那么所有访问者基类或者子类都要加visitYpu(ComputerPart* part)方法。也就是说,频繁加元素类型的场景不适合访问者模式

访问者模式代码稍多,但极其适合频繁扩展功能的场景。

这就是设计模式的价值不是为了复杂而复杂,是为了未来好扩展、好维护。

五.案例总结

|--------|-----------|--------------|
| | 传统写法 | 访问者模式 |
| 逻辑存放位置 | 写在每个元素类内部 | 统一放在访问者类 |
| 扩展新功能 | 必须修改所有元素类 | 只加新访问者,不改旧代码 |
| 代码耦合度 | 高 | 低 |
| 维护难度 | 难(越写越乱) | 易(职责清晰) |
| 符合设计原则 | 一般 | 完美符合开闭原则 |

  • 元素类型不怎么变,但经常加新功能 → 用访问者模式,比如:电脑部件(GPU、声卡)固定,但检测功能(版本、功能、温度、电压...)经常加。

  • 功能不怎么变,但经常加新元素类型 → 用传统写法

六.访问者模式优点

  • 符合开闭原则:新增操作仅需加访问者,无需修改元素类,代码稳定。
  • 数据与操作分离:元素只存数据,同类操作集中于访问者,逻辑内聚。
  • 可累积状态:访问者可记录统计 / 日志,不污染元素对象本身。

七.访问者模式缺点

  • 元素结构难扩展:新增元素需修改所有访问者,此时又违反开闭原则。
  • 破坏封装性:需暴露元素内部数据,削弱面向对象封装性。
  • 实现成本高:双分派逻辑复杂,理解和维护难度高于普通多态。
  • 耦合关系复杂:访问者依赖所有具体元素,双向依赖易造成逻辑绕。
相关推荐
workflower2 小时前
未来图景对制造系统提出全面理解、
设计模式·集成测试·软件工程·软件构建·制造·结对编程
浅念-2 小时前
Linux 进程与操作系统
linux·运维·服务器·网络·数据结构·笔记·网络协议
计算机安禾2 小时前
【数据结构与算法】第20篇:二叉树的链式存储与四种遍历(前序、中序、后序、层序)
c语言·开发语言·数据结构·c++·学习·算法·visual studio
顶点多余2 小时前
POSIX信号量+生产消费模型应用+环形缓冲区实现
linux·c++
刘若里2 小时前
【论文阅读】自适应稀疏自注意力——可直接用!
论文阅读·人工智能·笔记·深度学习·计算机视觉
滴_咕噜咕噜2 小时前
WPF项目实战视频《五》(主要为项目实战-客户端)
笔记
￰meteor3 小时前
【函数指针】
c++
Huangjin007_3 小时前
【C++类和对象(四)】手撕 Date 类:赋值运算符重载 + 日期计算
开发语言·c++