设计模式学习[15]---适配器模式
文章目录
- 前言
- 1.引例
- 2.适配器模式
- 2.1 对象适配器
- 2.2 类适配器
- 总结
前言
这个模式其实在日常生活中有点常见,比如我们的手机取消了 3.5 m m 3.5mm 3.5mm的接口,只留下了一个 T y p e − C Type-C Type−C的接口,但是我现在有一个 3.5 m m 3.5mm 3.5mm的耳机接口,那怎么让耳机和手机相连?
当然是买个转接头或者转接线,一头是
3.5
m
m
3.5mm
3.5mm圆孔一头是
t
y
p
e
−
c
type-c
type−c接口。
那这里的转接头或者转接线,充当的就是适配器。我手机只有type-c的接口,现在要让
3.5
m
m
3.5mm
3.5mm的插头适配
t
y
p
e
−
c
type-c
type−c,就需要这个适配器。
这篇博客就写一下适配器模式,看看在软件开发里面,什么时候用适配器,又是怎么用的适配器。
1.引例
我们日常写代码,一般都在debug版本下写,调试程序也是这样。但是我们要发布程序的时候,肯定是要release版本发布的。发布给客户的软件出现了bug,崩溃了,如何快速定位?这又不是我们VS里面一个F5开始调试就能搞定的,这时候就需要根据日志文件来判断了,说到这,其实就我近期开发的模块,日志量确实不太够,惭愧。
写日志我们通常是把他写到一个log.txt这样的文件里面去,现在定义这样的一个写日志文件操作的类
对于这个日志类,我们在main函数中一般可以这样用:
LogToFile* plog=new LogToFile();
plog->initfile();
plog->writetofile("向日志文件中写入一条日志");
plog->readfromfile();
plog->closefile();
delete plog;
如果程序运行很久,日志越来越多,各种模块的日志信息都往这个文件添加,就会导致日志文件过大,这时候需要将日志写到文件变成写到数据库。对于这种情况,其实就是新的设计与实现,具体类如下:
对于这个日志写到数据库的类,我们在main函数中一般可以这样用:
LogToDatabase* plog=new LogToDatabase();
plog->initdb();
plog->writetodb("向日志文件中写入一条日志");
plog->readfromdb();
plog->closedb();
delete plog;
上面是写日志的两种方式,我们可以看到他们的实现很不一样,一个对文件操作,一个对数据库操作。
现在考虑这种情况,数据库在某一天突然G了,可能因为网络问题导致无法和数据库连接。那上面的第二种写日志的方式似乎就用不了了,我们还得用回第一种方式,但这时候又来问题了。我们目前使用的方案是数据库方案,代码都是基于LogToDatabase类的接口,而LogToDatabase类的接口和LogToFile类又不一样,咋办?
这个就相当于我们耳机一直是用type-c连手机,突然有一天手机type-c口坏了,蓝牙坏了,我们只能用有线3.5mm接口耳机,这时候咋办?转接头呀!
2.适配器模式
在不改变老日志系统源码的情况下,通过引入适配器,将使用新日志系统的项目与老日志系统接驳起来,此时,适配器扮演一个中间人的角色,将项目中针对新日志系统的接口调用转换成对应的老日志系统的接口调用,从而达到新接口适配老接口的目的,这就是适配器模式的工作
接下来是考虑适配的实现方式了,这里适配器的实现分为两种:对象适配器
、类适配器
前者是通过类与类之间的组合关系,也就是一个类的定义中含有其他类型的成员变量。这种关系实现了委托机制,即成员函数把功能的实现委托给了其他类的成员函数,当然需要持有一个其他类的指针,才可以实现委托。
后者则是通过类与类之间的继承关系来实现接口的适配,适配器类和和适配者类之间是继承关系。
2.1 对象适配器
在适配器模式中,一般分为三种角色
Target(目标抽象型):
该类定义所需要暴露的接口,这些接口就是调用者希望使用的接口,也就是客户端需要用到的接口。这里是指LogToDatabase类。
Adaptee(适配者类):
这个类表示的是被适配的角色,通常是指老接口。需要用被适配。这里是指LogToFile类。
Adapter(适配器类):
这个就是适配器了,充当转换器的作用,是适配模式的核心。这里的作用就是把客户端针对LogToDatabase类的接口调用转换成对LogToFile(旧接口)的调用。
参考一下UML类图,在对象适配器中,我们需要让适配器继承目标抽象型LogToDatabase,因为它是对外暴露的接口。
其次,适配器需要有老接口类的指针,用来调用老接口。
现在具体看一下代码:
在这个类中,我们通过在适配器类LogAdapter中定义一个LogToFile的指针,在外层暴露的initdb,writetodb等接口中,调用老的接口。这样就做到了接口的适配,用同一套对外暴露的接口,实现底层的适配。
适配器模式与装饰模式有类似的地方,两者都使用了类与类之间的组合关系,但两者的实现意图是不同的,适配器模式是将原有的接口适配成另外一个接口,而装饰模式是对原有功能的增强,而且无论装饰多少层,装饰模式的调用接口始终不发生改变。
2.2 类适配器
先看UML,这里适配器类内部不再持有旧接口的指针,而也是用继承关系来做。
依照UML,我们的具体代码如下:
这里我们对于目标类LogToDatabase使用的是公有继承,对于旧接口LogToFile类,我们使用的是私有继承。
这里public继承表示的时一种 is-a 关系,也就是通过子类产生的对象一定也是一个父类对象,子类继承了父类的接口。
但是private继承就不是这种关系了,是一种组合关系,是根据…实现出的关系。
这里我们对旧接口私有继承就表示想通过LogToFile类实现出LogAdapter的意思。
这种多重继承的方式来做类适配器,在具体代码的实现中,我们可以直接调用LogToFile接口,不再需要特定的指针。看起来好像很不错,但是实际上是不提倡用类适配器的。
从灵活性上来说,类适配器不如对象适配器,因为private继承方式限制了LogAdapter能调用的LogToFile中的接口。
假如有下面的情况:
class ParClass{....}
class LogToFile:public ParClass{....}
采用对象适配器的话,我只需要在里面定一个ParClass* m_pfile指针,这个指针可以指向任何ParClass的子类对象,有时候这个子类对象可能不止一个。但如果是多重继承,我们需要继承的就不止这一个类了。
总结
一般来说,过多使用适配器模式并不见得是一件好事,因为从表面上看,调用的是A接口,但内部被适配成了调用B接口,这比较容易让人迷惑,一般都是在开发后期不得已才使用这种设计模式。所以,在很多情况下,如果方便对系统进行重构的话,那么以重构来取代适配也许更好。但软件开发中也存在时常要发布新版本的情况,新版本也存在与老版本的兼容性问题,有时完全抛弃老版本并不现实,所以才借助适配器模式使新老版本兼容。在遗留代码的复用、类库的迁移等工作方面,适配器模式仍旧能发挥巨大的作用。