C++设计模式_结构型模式_适配器模式Adapter

从本篇开始记录结构型模式。

结构型模式,该模式关注对象之间的组合关系,旨在解决如何构建灵活且可复用的类和对象结构。共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
适配器(Adapter)模式是一种结构型模式。生活中有很多适配器思想的实际应用,例如,各种转接头(usb转接头、hdmi转接头)、电源适配器(变压器)等,主要是用来解决不同的接口之间的兼容问题。设想一下,220V的市电不能直接给手机充电,引人一个电源适配器,把220V的市电转换为5V的电压,就可以给手机充电了。在软件开发领域,两个类之间也存在这种不兼容的问题,于是,就可以像生活中引人电源适配器那样引入一个适配器角色的类来解决这两个类之间的兼容性问题。

一个简单的例子

下面是一个简单的示例,这个类中只有日志的一些基础操作函数。

cpp 复制代码
class LogToFile
{
public:
	void initfile()
	{
		//做日志文件初始化工作,比如打开文件等等
		//.....
		cout << "日志初始化工作完成" << endl;
	}
	void writetofile(const char* pcontent)
	{
		//将日志内容写入文件
		//......
		cout << "pcontent 写入到了文件中" << endl;
	}
	void readfromfile()
	{
		//从日志中读取一些信息
		//......
		cout << "从文件中读取了一些信息" << endl;
	}
	void closefile()
	{
		//关闭日志文件
		//......
		cout << "关闭日志文件" << endl;
	}

	// ......
};
void test()
{
	LogToFile logtofile;
	logtofile.initfile();
	logtofile.writetofile("hello");
	logtofile.readfromfile();
	logtofile.closefile();
	/*
	日志初始化工作完成
	pcontent 写入到了文件中
	从文件中读取了一些信息
	关闭日志文件
	*/
}

随着项目规模的不断增加,要记录的日志信息也逐渐增多,单纯地向日志文件中记录日志信息会导致日志文件膨胀得过大,不方便管理和查看。于是准备对项目中的日志系统进行升级改造,即从原有的将日志信息写入文件改为将日志信息写人到数据库。改造后的新日志系统如下:

cpp 复制代码
class LogToDatabase
{
public:
	void initdb()
	{
		//连接数据库,做一些基本的数据库连接设置等
		//......
		cout << "连接数据库" << endl;
	}
	void writetodb(const char* pcontent)
	{
		//将日志内容写入数据库
		//......
		cout << "pcontent 写入到了数据库中" << endl;
	}
	void readfromdb()
	{
		//从数据库中读取一些日志信息
		//......
		cout << "从数据库中读取了一些日志信息" << endl;
	}
	void closedb()
	{
		//关闭到数据库的链接
		//......
		cout << "关闭到数据库的链接" << endl;
	}
};
void test2()
{
	LogToDatabase logtodatabase;
	logtodatabase.initdb();
	logtodatabase.writetodb("hello");
	logtodatabase.readfromdb();
	logtodatabase.closedb();
	/*
		连接数据库
		pcontent 写入到了数据库中
		从数据库中读取了一些日志信息
		关闭到数据库的链接
	*/
}

现在遇到一个问题,数据库断电或者电缆出现问题,导致了新的日志系统不能使用了。可以使用LogToFile类来解决上面问题,但是现在项目中所有的位置都使用的是 LogToDatabase类的接口,而LogToFile类接口与LogToDatabase接口又不完全相同,该怎么办呢?现在的解决办法有三种:

1 将所有的接口改为LogToFile类的接口,这种改动较大;

2 在LogToDataBase类中增加新接口来支持对日志文件的读写,也就是把LogToFile 中的接口,在LogToDataBase 中重新实现一遍,这比较麻烦。

3 使用适配器解决。在该模式中,通过引人适配器类,把LogToDatabase类中,诸如对 writeToDb、readfromDb等成员函数的调用转换为对LogToFie类中,诸如对 WriteToFile readfromFile等成员函数的调用, 从而实现接口调用的目的。

适配器方式如下:

cpp 复制代码
class LogToFile
{
public:
	void initfile()
	{
		//做日志文件初始化工作,比如打开文件等等
		//.....
		cout << "日志初始化工作完成" << endl;
	}
	void writetofile(const char* pcontent)
	{
		//将日志内容写入文件
		//......
		cout << "pcontent 写入到了文件中" << endl;
	}
	void readfromfile()
	{
		//从日志中读取一些信息
		//......
		cout << "从文件中读取了一些信息" << endl;
	}
	void closefile()
	{
		//关闭日志文件
		//......
		cout << "关闭日志文件" << endl;
	}
	// ......
};

class LogToDatabase
{
public:
	virtual void initdb() = 0; //不一定非纯虚函数
	virtual void writetodb(const char* pcontent) = 0;
	virtual void readfromdb() = 0;
	virtual void closedb() = 0;

	virtual ~LogToDatabase()
	{

	}
};

// 类适配器
class LogAdapter  : public LogToDatabase
{
public:
	//构造函数
	LogAdapter(LogToFile* pfile) //形参是老接口所属类的指针
	{
		m_pfile = pfile;
	}
	virtual void initdb()
	{
		cout << "在LogAdapter::initdb()中适配LogToFile::initfile()" << endl;
		m_pfile->initfile();
	}
	virtual void writetodb(const char* pcontent)
	{
		cout << "在LogAdapter::writetodb()中适配LogToFile::writetofile()" << endl;
		m_pfile->writetofile(pcontent);
	}
	virtual void readfromdb()
	{
		cout << "在LogAdapter::readfromdb()中适配LogToFile::readfromfile()" << endl;
		m_pfile->readfromfile();
	}
	virtual void closedb()
	{
		cout << "在LogAdapter::closedb()中适配LogToFile::closefile()" << endl;
		m_pfile->closefile();
	}

private:
	LogToFile* m_pfile;  // 聚合关系
};

void test()
{
	LogToFile logtofile;
	LogAdapter logadapter(&logtofile);
	logadapter.initdb();
	logadapter.writetodb("hello");
	logadapter.readfromdb();
	logadapter.closedb();
	/*
			在LogAdapter::initdb()中适配LogToFile::initfile()
			日志初始化工作完成
			在LogAdapter::writetodb()中适配LogToFile::writetofile()
			pcontent 写入到了文件中
			在LogAdapter::readfromdb()中适配LogToFile::readfromfile()
			从文件中读取了一些信息
			在LogAdapter::closedb()中适配LogToFile::closefile()
			关闭日志文件
	*/
}

通过代码可以看到,在test中的代码仅仅做了很小的变动,其中对接口,例如initdb、writetodb、readfromdb、closedb后调用更是没有发生改动,通过引人适配器类,实际调用的接口是 LogToFile 类的 initfile,writetofile、readfromfile、closefile。

实际上,适配器的能力简而言之就是能够将对一种接口的调用转为对另一种接口的调用。使用适配器转换两个类时,这两个类必须是相关的两个类。

引入适配器(Adapter)模式

上面的代码完成了新老日志系统的转换。在不改变老日志系统源码的情况下,通过引人适配器,将使用新日志系统的项目与日志系统连接起来,此时,适配器扮演一个中间人的角色,将项目中针对日志系统的接口转换成对应的老日志系统的接口调用,从而达到新接口适配老接的目的,这就是适配模式的工作。

两个独立的日志系统;

使用适配器连接起来。

适配器的定义:将一个类的接口转换成客户希望的另外一个接口。该模式使得原本因为接口不兼容而不能一起工作的类可以一起工作(在上述范例中,不一起工作的类指的就是 LogToDatabase和LogToFile类)。适配器模式还有一个别名叫作包装器(Wrapper)。

适配器的UML图:

【上图中,LogAdaper和LogToFile之间是聚合关系,因为在LogAdaper中保存了LogToFile的指针】

适配器模式中包含3种角色:

1 Target(日标抽象类):该类定义所需要暴露的接口(诸如initdb、writetodbteadfromdb、closedb等)。

2 Adaptee(适配者类): 该类扮演着被适配的角色,其中定义了一个或多个已经存在,的接口(老接口),这些接口需要适配(对其他接口的调用转换成对这些接口的调用)。

3 Adapter(适配器类): 注意英文字母的拼写区别于Adaptee(适配者类)。适配器类是一个包装类,扮演着转换器的角色,是适配器模式的实现核心,用于调用另一个接口(包装适配者)。

适配器模式与装饰模式有类似的地方,两者都使用了类与类之间的组合关系,但两者实现意图是不同的,适配器模式是将原有的接口适配成另外一个接口,而装饰模式是对原有功能的增强,而且无论装饰多少层,装饰模式的调用接口始终不发生改变。

类适配器

适配器模式依据实现方式分为两种:一种是对象适配器,另一种是类适配器。前边是对象适配器,这种实现方式使用的是类与类之间的聚合关系,实现了委托机制。LogAdapter类中包含了LogToFile指针, 可以认为 LogToFile是LogAdapter的一部分。类适配器是通过继承方式来实现接口的适配:

cpp 复制代码
class LogToFile
{
public:
	void initfile()
	{
		//做日志文件初始化工作,比如打开文件等等
		//.....
		cout << "日志初始化工作完成" << endl;
	}
	void writetofile(const char* pcontent)
	{
		//将日志内容写入文件
		//......
		cout << "pcontent 写入到了文件中" << endl;
	}
	void readfromfile()
	{
		//从日志中读取一些信息
		//......
		cout << "从文件中读取了一些信息" << endl;
	}
	void closefile()
	{
		//关闭日志文件
		//......
		cout << "关闭日志文件" << endl;
	}
	// ......
};

class LogToDatabase
{
public:
	virtual void initdb() = 0; //不一定非纯虚函数
	virtual void writetodb(const char* pcontent) = 0;
	virtual void readfromdb() = 0;
	virtual void closedb() = 0;

	virtual ~LogToDatabase()
	{

	}
};

class LogAdapter : public LogToDatabase , private LogToFile
{  // private 继承,父类的public protected成员函数在子类中变成private成员函数
public:
	virtual void initdb()
	{
		cout << "在LogAdapter::initdb()中适配LogToFile::initfile()" << endl;
		initfile();
	}
	virtual void writetodb(const char* pcontent)
	{
		cout << "在LogAdapter::writetodb()中适配LogToFile::writetofile()" << endl;
		writetofile(pcontent);
	}
	virtual void readfromdb()
	{
		cout << "在LogAdapter::readfromdb()中适配LogToFile::readfromfile()" << endl;
		readfromfile();
	}
	virtual void closedb()
	{
		cout << "在LogAdapter::closedb()中适配LogToFile::closefile()" << endl;
		closefile();
	}
};
void test()
{
	LogAdapter logadapter;
	logadapter.initdb();
	logadapter.writetodb("hello");
	logadapter.readfromdb();
	logadapter.closedb();
	/*
	在LogAdapter::initdb()中适配LogToFile::initfile()
	日志初始化工作完成
	在LogAdapter::writetodb()中适配LogToFile::writetofile()
	pcontent 写入到了文件中
	在LogAdapter::readfromdb()中适配LogToFile::readfromfile()
	从文件中读取了一些信息
	在LogAdapter::closedb()中适配LogToFile::closefile()
	关闭日志文件
	*/
}

从代码中可以看到,LogAdapter使用了多重继承,以public(公有继承)的方式继承

LogToDatabase, public继承所作表的是一种is-a关系,也就是通过子类产生的对象一定也是一个父类对象(子类继承了父类的接口)。同时,LogAdapter 还以 private(protected 也可以)的方式继承了LogToFile类, private继承关系就不是一种is-a关系了,而是一种组合关系,更明确地说,是组合关系中的:implemented-in-terms-of(根据......实现出)关系,这里的private继承就表示想通过LogToFile类实现出LogAdapter的意思。

适配器模式的扩展运用

使用适配器模式并不一定是好事,而是在开发后期不得以才使用这种设计模式。但软件开发中也存在时常要发布新版本的情况,新版本也存在与老版本的兼容性问题,有时完全抛弃老版本并不现实,所以才借助适配器模式使新老版本兼容。在遗留代码的复用、类库的迁移等工作方面,适配器模式仍旧能发挥巨大的作用。

C++标准库中大量使用了适配器,容器适配器,算法适配器,迭代器适配器等。

(1) 容器适配器: 对于容器中的双端队列deque,它既包含了堆栈stack的能力也包含了队列queue的能力,在实现stack和queue 源码时,只需要利用既有的 deque 源码并进行适当的改造(减少一些东西)。因此stack和queue都可以看作容器适配器。

cpp 复制代码
_EXPORT_STD template <class _Ty, class _Container = deque<_Ty>>
class queue {
    void pop() noexcept(noexcept(c.pop_front())) /* strengthened */ {
        c.pop_front();
    }

    void swap(queue& _Right) noexcept(_Is_nothrow_swappable<_Container>::value) {
        using _STD swap;
        swap(c, _Right.c); // intentional ADL
    }

    _NODISCARD const _Container& _Get_container() const noexcept {
        return c;
    }

protected:
    _Container c{};
};

从源码中可以看出,queue队列的pop() 就是 调用了 deque的pop_front() 方法;'

(2)算法适配器,std::bind(绑定器)就是一个典型的算法适配器;

(3)迭代器适配器:例如reverse_iterator(反向迭代器),其实现只是对迭代器iterator的一层简单封装。

相关推荐
bkspiderx2 小时前
C++设计模式之结构型模式:代理模式(Proxy)
c++·设计模式·代理模式
1710orange2 小时前
java设计模式:适配器模式
java·设计模式·适配器模式
Cauhele浅能3 小时前
【嵌入式C快捷键设计】表驱动法实现
c语言·设计模式
庸人自扰614 小时前
常见设计模式讲解
设计模式
bkspiderx4 小时前
C++设计模式之行为型模式:解释器模式(Interpreter)
c++·设计模式·解释器模式
倔强菜鸟5 小时前
2025.8.10-学习C++(一)
开发语言·c++·学习
ZXF_H5 小时前
C/C++预定义宏与调试日志输出模板
开发语言·c++·日志·调试·预定义宏
2401_841495646 小时前
【数据结构】顺序表的基本操作
数据结构·c++·算法·顺序表·线性表·线性结构·顺序表的基本操作
小糖学代码6 小时前
STL的list模拟实现(带移动构造和emplace版本)
c语言·数据结构·c++·windows·list