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

文章目录

  • 一、引言
  • 二、适配器模式
  • 三、类适配器
  • 四、总结

一、引言

适配器模式是一种结构型设计模式,它在日常生活中有着广泛的应用,比如各种转换接头和电源适配器,它们的主要作用是解决接口不兼容的问题。就像使用电源适配器将220V的市电转换为5V电压来给手机充电一样,适配器模式用于解决两个类之间的不兼容问题。在C++ STL(标准模板库)中,适配器是六大组件之一,这六大组件包括容器、算法、迭代器、仿函数、适配器和空间适配器。适配器又可以细分为容器适配器、函数适配器和迭代器适配器。


二、适配器模式

适配器模式能使接口不兼容的对象能够相互合作。

假设公司某个对外提供服务的项目需要记录一些日志信息,以方便运营人员查看或者日后对项目的某些行为进行追溯。日志准备写到一个固定的文件中,于是,程序开发人员向项目中增加了一个LogToFile类来实现日志文件的相关操作(日志系统),代码如下:

class LogToFile {
public:void initfile() { /* 初始化文件日志 */ }void writetofile(const string& message) { /* 写入消息到文件 */ }void readfromfile() { /* 从文件读取消息 */ }void closefile() { /* 关闭文件 */ }//...  
};

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

class LogToDatabase {
public:void initdb() {}void writetodb(const string&) {}void readfromdb() {}void closedb() {}//...  
};

新的日志系统(LogToDatabase类)是将所有日志信息写人到数据库或者从数据库中读取日志信息,代码中凡是涉及与日志相关的类,也全部从以往的使用LogToFile类变成了使用LogToDatabase类。在主函数中,我们这样调用:

LogToDatabase* logdb = new LogToDatabase();
logdb->initdb();
//....

但有一天,突然遇到了一些意外的情况或者出现了一些特殊需求,机房突然断电导致数据库中的数据发生了损坏无法正确读写。需要从以往使用的LogToFile类所生成的日志文件中读取一些日志信息。

所有使用了LogToDatabase类的代码行要么无法应付意外发生的情况,要么无法实现特殊需求。所以在这种情况下使用回LogToFile类可以解决上面的两个问题,至少是能够临时解决。但问题是现在所有项目中的代码使用的都是LogToDatabase类,而LogToDatabase类的接口(成员函数)与LogToFile类的接口又完全不同,怎么办呢?

若把接口全部改回LogToFile类改动量很大,而且数据库恢复之后又得改回去。若修改LogToDatabase类,把以往LogToFile类中实现的功能融合到LogToDatabase类中来,但是这样也免不了修改调用该类的函数的地方。

此时我们就需要引入适配器模式,在该模式中,通过引入适配器类,把LogToDatabase类中,诸如对writetodbreadfromdb等成员函数的调用转换成对LogToFile类中,诸如对writetofilereadfromfile等成员函数的调用,从而达到直接使用LogToFile类中的接口的目的。这样做之后,main主函数中与LogToDatabase类相关的代码行只需要做非常小的调整,所调用的成员函数名都不需要改变。看一看采用适配器模式后代码如何修改。首先重新实现LogToDatabase类,但在适配器模式中该类并不是用于读写数据库日志,而是用于作为父类提供一些供子类使用的接口。

class LogToDatabase {
public:virtual void initdb() = 0;virtual void writetodb(const string&) = 0;virtual void readfromdb() = 0;virtual void closedb() = 0;virtual ~LogToDatabase(){}//...  
};

上述的LogToDatabase中定义了一些接口,这些接口都是当前项目中使用的操作日志的接口,我们可以称这些接口为目标接口(新接口)。LogToFile类的内容不变,其中的成员函数(接口)可以称为老接口。接着引人适配器类LogAdapter,其父类为LogToDatabase,应注意该类的构造函数中的形参类型(LogToFile类型)

class LogAdapter : public LogToDatabase {
public:LogAdapter(LogToFile  log) : m_pfile(make_unique<LogToFile>(log)) {m_pfile->initfile(); // 初始化文件日志}virtual void initdb() override {// 实现数据库初始化}virtual void writetodb(const string& message) override {m_pfile->writetofile(message); // 将消息适配为文件日志}virtual void readfromdb() override {m_pfile->readfromfile(); // 适配从文件读取}virtual void closedb() override {m_pfile->closefile(); // 关闭文件}private:unique_ptr<LogToFile> m_pfile;
};

此时我们仅对代码进行一小点改动,其中对接口,例如 initdbwritetodb等没有发生改动,通过适配器类,实际调用的接口都是文件日志的。

LogToFile  logfile;
LogToDatabase* plogdb2 = new LogAdapter(logfile);
plogdb2->initdb();
plogdb2->writetodb("向数据库中写人一条日志,实际是向日志文件中写人一条日志");
plogdb2->readfromdb();
plogdb2->closedb();
delete plogdb2;

引入适配器模式的定义(实现意图):将一个类的接口转换成客户希望的另外一个接口。该模式使得原本因为接口不兼容而不能一起工作的类可以一起工作。

根据上述对LogToDatabase类接口的调用转换为对LogToFile类接口的调用:

在这里插入图片描述

我们使用了适配器类实现了对接口实际调用的转换。也就是说,当需要把被适配的接口(如writetofilereadfromfile)应用到当前环境下,就需要配适配器。

  • 目标抽象类Target):该类定义所需要暴露的接口(诸如initdb、writetodb、readfromdb、closedb等)。这些接口其实就是未来的接口或者说是调用者希望使用的接口,将被客户端或说调用者(例如,上述范例中main主函数中的调用代码)调用。这里指LogToDatabase类。
  • 适配者类Adaptee):该类扮演着被适配的角色,其中定义了一个或多个已经存在的接口(老接口),这些接口需要适配(对其他接口的调用转换成对这些接口的调用)。这里指LogToFile类(旧类)。在适配器模式中,适配者类不限于一个,也可以有多个。
  • 适配器类Adapter):注意英文字母的拼写区别于Adaptee(适配者类)。适配器类是一个包装类,扮演着转换器的角色,是适配器模式的实现核心,用于调用另一个接口(包装适配者)。该类对 Adaptee 和 Target 进行适配。这里所说的适配,指的就是把客户端针对LogToDatabase类中接口的调用转换成对LogToFile类中接口的调用。适配器类这里指LogAdapter类。

适配器结构

在这里插入图片描述

  1. 客户端Client) 是包含当前程序业务逻辑的类。
  2. 客户端接口Client Interface) 描述了其他类与客户端代码合作时必须遵循的协议。
  3. 服务Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能。
  4. 适配器Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。
  5. 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。

三、类适配器

适配器模式依据实现方式分为两种:一种是对象适配器,另一种是类适配器。前面所讲述的适配器模式是对象适配器(主要说的是LogAdapter类),这种适配器模式的实现用了类与类之间的组合关系,也就是一个类的定义中含有其他类类型的成员变量。这种关系实现了委托机制(即成员函数把功能的实现委托给了其他类的成员函数,当然需要持有一根其他类的指针,才能实现委托)。

在前面的范例中,可以理解为LogAdapter对象包含着一个LogToFile对象。这是一种委托机制(即成员函数把功能的实现委托给了其他类的成员函数,当然需要持有一根其他类的指针,才能实现委托)。

对于类适配器,则是通过类与类之间的继承关系来实现接口的适配,即适配器类和适配者类之间是继承关系。我们改造上面的适配器:

class LogAdapter : public LogToDatabase, private LogToFile {
public:LogAdapter() {// 初始化文件日志}virtual void initdb() override {// 实现数据库初始化}virtual void writetodb(const string& message) override {writetofile(message); // 将消息适配为文件日志}virtual void readfromdb() override {readfromfile(); // 适配从文件读取}virtual void closedb() override {closefile(); // 关闭文件}
};

在调用时:

LogToDatabase* plogdb2 = new LogAdapter();
plogdb2->initdb();
plogdb2->writetodb("向数据库中写人一条日志,实际是向日志文件中写人一条日志");
plogdb2->readfromdb();
plogdb2->closedb();
delete plogdb2;

执行起来,结果不变。

从代码中可以看到,LogAdapter使用了多重继承,以public(公有继承)的方式继承了LogToDatabasepublic继承所代表的是一种is-a关系,也就是通过子类产生的对象一定也是一个父类对象(子类继承了父类的接口)。LogAdapter还以privateprotected也可以)的方式继承了LogToFile类,private继承关系就不是一种is-a关系了,而是一种组合关系。这里的private继承就表示想通过LogToFile类实现出LogAdapter的意思。

在这里插入图片描述

一般来说,不适应类适配器。因为它不如对象适配器灵活,private继承方式限制了LogAdapter能调用LogToFile的接口,而对象适配器中采用指针就灵活的多。

类适配器结构

在这里插入图片描述


四、总结

适配器模式在软件开发中使用得比较广泛,但并不是总是最佳选择。过多地使用适配器模式可能会导致混淆,因为从外部看调用的是 A 接口,但内部却适配成了 B 接口。这种情况在项目后期重构时通常更常见,因此在可能的情况下,重构代码可能比使用适配器更好。

然而,在软件开发中,发布新版本时常常会面临与旧版本的兼容性问题。完全抛弃旧版本并不现实,因此适配器模式可以帮助实现新旧版本的兼容。尤其在遗留代码的复用和类库迁移等方面,适配器模式发挥了重要作用。

尽管适配器模式有时让人感到无奈,仿佛是在无法修改接口的情况下才被迫使用,但在某些情况下,它实际上可以帮助实现更实质性的功能。这一点在 C++ 标准库(STL)中得到了很好的体现。

STL 包含六个主要组件:容器、算法、迭代器、函数对象(仿函数)、内存分配器和适配器。C++ 标准库中有许多适配器,主要分为容器适配器、算法适配器和迭代器适配器。适配器的作用是对现有的东西进行适当的修改,比如增加或减少某些内容,从而变成一个适配器。

  • 容器适配器:std::stack:基于底层容器(如 std::dequestd::vector)实现的栈(后进先出)结构。
  • 算法适配器:如,std::bind绑定器就是一个典型的算法适配器。
  • 迭代器适配器:如reverse_iterator(反向迭代器),其实现只是对迭代器iterator进行了封装。

桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。

适配器可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。

适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。

外观模式为现有对象定义了一个新接口, 适配器则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。

桥接、 状态模式和策略模式 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

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

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

相关文章

交换机如何实现2.5G网络传输速率和网络变压器有关吗

华强盛电子导读&#xff1a;I19926430038 交换机实现2.5G网络传输速率涉及多个因素&#xff0c;其中包括硬件设计、端口支持、传输介质以及网络协议等。网络变压器在其中扮演了一个重要的角色&#xff0c;但并不是唯一的因素。 1. **硬件设计**&#xff1a;交换机需要有支持2.…

Centos环境下安装docker

本文演示离线版安装用于没有网络环境的系统 在线版安装可参考以下链接 https://www.runoob.com/docker/centos-docker-install.html 一、docker离线安装 1、下载docker离线安装包 docker下载地址&#xff1a; Docker版本下载 选择版本 下载后上传至服务器 百度网盘下载…

微软官方 .NET 混淆软件 Dotfuscator

微软官方 .NET 混淆软件 Dotfuscator 1、前言2、Dotfuscator 特色2.1、强大的保护2.2、不需要顾问2.3、世界一流的支持2.4、广泛的平台支持 3、Dotfuscator 功能介绍3.1、.NET Obfuscator3.2、篡改防御和提示3.3、监控性能和使用情况3.4、Silverpght XAML Obfuscatio3.5、WPF B…

深入浅出:SM4 加密算法及其多种工作模式详解

深入浅出&#xff1a;SM4 加密算法及其多种工作模式详解 引言 SM4 是中国国家密码管理局定义的对称分组加密算法&#xff0c;广泛应用于无线局域网安全协议等领域。作为中国商用密码算法之一&#xff0c;SM4 采用 128 位的分组长度和密钥长度&#xff0c;提供了高效且安全的加…

摄像机实时接入分析平台LiteAIServer视频智能分析软件诊断噪声检测

在科技日新月异的今天&#xff0c;视频监控系统的应用日益广泛&#xff0c;从公共安全到家庭防护&#xff0c;从生产线管理到交通监控&#xff0c;视频监控已经成为现代社会不可或缺的一部分。然而&#xff0c;噪声问题一直是影响视频画面清晰度和可用性的关键因素。为了解决这…

NumPy安装

1.NumPy简介 NumPy(Numerical Python) 是 Python 语言的扩展程序库&#xff0c;支持大量维度数组与矩阵运算&#xff0c;此外也针对数组运算提供大量的数学函数库。 NumPy 的前身 Numeric 最早由 Jim Hugunin 与其它协作者共同开发&#xff0c;2005 年&#xff0c;Travis Oliph…

推荐几款TOP级AI驱动的单元测试工具

这篇文章&#xff0c;我想对开发人员人员来说更有帮助&#xff0c;毕竟开发同学“苦单元测试久已”&#xff01; 软件开发是一项创造性的工作&#xff0c;但其中也包含着许多乏味的任务。其中最乏味的莫过于编写“单元测试”了&#xff0c;用于验证软件组件是否按预期工作。单…

C#的Event事件示例小白级剖析

1、委托Delegate 首先说一下delegate委托&#xff0c;委托是将方法作为参数进行传递。 // 定义了一个委托类型public delegate void MyDelegate(int num);// 定义了一个啥也不干的委托实例public MyDelegate m_delegate _ > {};// 定义了一个和委托相同格式的方法public …

JWT-混淆算法

jwt - RS256&#xff08;RSA SHA-256&#xff09; 题目来源&#xff1a;DownUnderCTF2021 Web jwt 外国的比赛&#xff0c;找不到线上的环境了&#xff0c;github中有Docker&#xff0c;拖下来用docker生成一个本地环境 原题wp链接&#xff1a; https://ctftime.org/write…

物联网开发教程专栏介绍与专栏说明——列表目录查阅(持续更新)

阿齐Archie《物联网开发&#xff1a;完整实现单片机通信模组云服务器智能应用软件》专栏 为方便查阅学习本专栏&#xff0c;特整理专栏介绍与专栏说明 一、专栏介绍 物联网开发教程专栏目前有P1和P2系列&#xff0c;P1系列为《手把手完整实现STM32ESP8266MQTT阿里云APP应用》…

Matlab实现海洋捕食者优化算法(MPA)求解路径规划问题

目录 1.内容介绍 2.部分代码 3.实验结果 4.内容获取 1内容介绍 海洋捕食者优化算法&#xff08;MPA&#xff09;是一种基于自然界海洋生物捕食行为的优化算法&#xff0c;它通过模拟海洋捕食者如鲨鱼、海豚等在寻找猎物时的群体协作和个体行为来探索最优解。MPA因其出色的全局…

数据结构(8.5_1)——归并排序

定义 归并&#xff1a;把两个或多个已经有序的序列合并成一个 归并后&#xff1a; 2路归并 把两个或多个已经有序的序列合并成一个 m路归并 m路归并&#xff0c;每选出一个元素需要对比关键字m-1次 归并排序(手算) 代码实现 算法效率分析 2路归并的“归并树”——形态上…

软件开发详解:基于食堂采购系统源码开发现代化供应链管理平台实战

下文&#xff0c;小编将从食堂采购系统源码切入&#xff0c;为大家详细解答如何开发现代化供应链管理平台。 一、供应链管理平台的功能需求 供应链管理平台的设计&#xff0c;需要满足企业从原料采购到成品交付的完整业务流程。以下是基于食堂采购系统源码开发的供应链管理平…

【99.9%解决】vue3+vite+typescript+vscode使用@alias路径别名配置不正确导致红色波浪线的解决办法

相信很多人设置了别名“”后在编辑器内产生了大量的红色波浪线&#xff0c;警告无法读取相关模块。网上针对这个问题都没有好好分析原因&#xff0c;并且提供真正理解之下的解决方案。我在历经各种失败后&#xff0c;总结出这篇文章&#xff0c;希望对大家有所帮助。 当然我因为…

「Mac畅玩鸿蒙与硬件18」鸿蒙UI组件篇8 - 高级动画效果与缓动控制

高级动画可以显著提升用户体验&#xff0c;为应用界面带来更流畅的视觉效果。本篇将深入介绍鸿蒙框架的高级动画&#xff0c;包括弹性动画、透明度渐变和旋转缩放组合动画等示例。 关键词 高级动画弹性缓动自动动画缓动曲线 一、Animation 组件的高级缓动曲线 缓动曲线&#…

BFS解决拓扑排序(3)_火星词典

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 BFS解决拓扑排序(3)_火星词典 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录…

QT中使用图表之QChart绘制X轴为日期时间轴的折线图

显然X轴是日期时间轴的话&#xff0c;那么我们使用的轴类就得是QDateTimeAxis QChart中日期时间轴的精度是毫秒 因此图表里面的数据的x值需要是一个毫秒数&#xff0c;才能显示出来 --------------------------------------------------------------------------------------…

C++现代教程七之模块

优点 编译时间减少&#xff1a;模块消除了重复解析和编译头文件的需要&#xff0c;从而显著减少了编译时间。特别是在大型项目中&#xff0c;这一点尤为重要。更好的封装性&#xff1a;模块允许更严格的封装&#xff0c;可以明确地控制哪些符号对外可见。这有助于减少命名冲突和…

ML 系列:第 18 部 - 高级概率论:条件概率、随机变量和概率分布

文章目录 一、说明二、关于条件概率2.1 为什么我们说条件概率&#xff1f;2.2 为什么条件概率在统计学中很重要 三、 随机变量的定义3.1 定义3.2 条件概率中的随机变量 四、概率分布的定义五、结论 一、说明 条件概率是极其重要的概率概念&#xff0c;它是因果关系的数学表述&…

Spring @RequestMapping 注解

文章目录 Spring RequestMapping 注解一、引言二、RequestMapping注解基础1、基本用法2、处理多个URI 三、高级用法1、处理HTTP方法2、参数和消息头处理 四、总结 Spring RequestMapping 注解 一、引言 在Spring框架中&#xff0c;RequestMapping 注解是构建Web应用程序时不可…