第8章 结构型模式—适配器模式
1. 适配器模式(Adpater pattern)的定义
(1)将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原来由于接口不兼容而不能一起工作的那些可以一起工作。
(2)适配器模式的结构和说明
①Client客户端,调用自己需要的领域接口Target
②Target:定义客户端需要的跟特定领域相关的接口
③Adaptee:己经存在的接口,通常能满足客户端的功能要求,但接口与客户端要求的特定领域接口不一致,需要被适配。
④Adapter:适配器,把Adaptee适配成Client需要的Target。可分为对象适配器(与Adpatee是组合关系)和类适配器(与Adaptee是继承关系)
(3)思考适配器模式
①适配器模式的本质:转换匹配,复用功能。它通过转换调用己有的实现,从而能把己有的实现匹配成需要的接口,使之能满足客户端的需要。
②适配器模式的动机:将“一些现存的对象”放在新的环境中应用,但是新环境要求的接口是这些现存的对象所不满足的。为了应对这种“迁移的变化”,既能利用现有对象的良好实现,同时又能满足新的应用环境所要求的接口而提出的。
③适配器模式的意图:将一个接口转换成客户希望的另一个接口。Adapter使原本由于接口不兼容而不能在一起工作的那些类可以在一起工作。
④Adaptee和Target的关系:被适配的接口Adaptee和适配的目标接口Target是没有关联的,他们可以是完全不同的两个接口。
【编程实验】绘图工具(整合己有的工具)
//结构型模式:适配器模式
//绘制工具:整合己有的类
#include <iostream>
#include <string>
using namespace std;
//************************************绘图接口********************************
class Shape
{
public:
virtual void draw() = 0;
};
//新的类直接继承自Shape
//矩形
class Rectangle : public Shape
{
public:
void draw()
{
cout << "Rectangle: Shape.draw()..." << endl;
}
};
//三角形
class Triangle : public Shape
{
public:
void draw()
{
cout << "Triangle:Shape.draw()..." << endl;
}
};
//画线
class Line : public Shape
{
public:
void draw()
{
cout << "Line:Shape.draw()..." << endl;
}
};
//***************************************己有的旧的类,但接口与shape不同
class ICircle
{
public:
virtual void drawCircle() = 0;
};
//原来己写好的旧类(来自ICircle)接口
class Circle : public ICircle
{
public:
void drawCircle()
{
cout << "ICircle.drawCircle()" << endl;
}
};
//适配器,用来将ICircle转换为Shape接口
class CircleAdapter :public Shape
{
ICircle* adaptee; //持有Adaptee接口
public:
CircleAdapter(ICircle* adaptee){this->adaptee = adaptee;}
void draw()
{
adaptee->drawCircle(); //委托drawCircle接口去实现
}
};
int main()
{
//画线、矩形、三角形
Shape* sp = new Triangle(); //new Line()、new Rectangle()
sp->draw();
//通过适配器来画圆
Circle* cc = new Circle();
Shape* circle = new CircleAdapter(cc);//把这个adaptee对象包含进去
circle->draw();
delete sp;
delete cc;
delete circle;
return 0;
}
2. 适配器模式的实现
(1)适配器的常见实现
①适配器通常是一个类,一般会让适配器类去实现Target接口,然后在适配器的具体实现里面调用Adaptee。
②适配器通常是一个Target类型,而不是Adaptee类型(通过组合,而不是继承)
(2)适配多个Adaptee
适配器在适配的时候,可以适配多个Adaptee,也就是说实现某个新的Target功能的时候,需要调用多个模块的功能,适配多个模块的功能才能满足新接口的需要。
(3)缺省适配
缺省适配指的是为一个接口指供缺省的实现,就不用直接去实现接口,而是采用继承这个缺省适配对象,从而让子类可以有选择地去覆盖实现需要的接口,对于不需要的方法,使用缺省适配的方法就可以了。
(4)双向适配器:就是把Adaptee适配成Target,也可以把Target适配成Adaptee。
3. 对象适配器和类适配器
(1)实现上的差异
①对象适配器的实现:依赖于对象组合。即Adapter和Adaptee是组合关系。
②类适配器的实现:采用继承,即Adapter和Adaptee是继承关系,同时Adpater还实现了Target的接口,在C++中表现为多重继承。
(2)类适配器和对象适配器的权衡
①从实现上:类适配器使用对象的继承方式,是静态定义方式;而对象适配器使用对象组合方式,是动态组合的方式。
②对于类适配器,由于Adapter直接继承了Adaptee,使得适配器不能和Adaptee的子类一起工作,因为继承是静态的关系,当适配器继承了Adaptee后,就不能再去处理Adaptee的子类了。
③对于对象适配器,允许一个Adapter和多个Adaptee,包括Adaptee和它所有的子类一起工作。因为对象适配器采用的是对象组合的关系,只要对象类型正确,是不是子类都无所谓。
④对于类适配器,Adapter可以重定义Adaptee的部分行为,相当于子类覆盖父类的部分实现方法。但对于对象适配器,要重定义Adaptee的行为比较困难,这种情况需要定义Adaptee的子类来实现重定义,然后让适配器组合子类。
⑤对于类适配器,仅仅引入了一个对象,并不需要额外的引用来间接得到Apdatee。而对象适配器,需要额外的引用来间接得到Adaptee。
(3)建议:尽量使用对象适配器的实现方法。当然,具体问题具体分析,根据需要来选用实现,最合适的才是最好的。
4. 适配器模式的优缺点
(1)优点
①更好的复用性:如果功能己经有了,只是接口不兼容,那么通过适配器模式就可以让这些功能得到更好的复用。
②更好的扩展性:在实现适配器功能时,可以调用自己开发的功能,从而自然地扩展了系统的功能。
③增加了类的透明性:我们访问的是Target的接口,但是具体的实现都委托给了Adaptee,而这对于高层次模块是透明的,也是它不需要关心的。
(2)缺点
过多地使用适配器,会让系统非常零乱,不容易整体进行把握。如明明看到调用的是A接口,其实内部被适配成B接口来实现。
5. 适配器模式的使用场景
①只要记住一点:想要修改一个己经存在的接口时,可以考虑用适配器去转换成需要的接口。
②STL的stack/queue就是使用适配器模式实现的,stack/queue就是Target(或者说Adapter,因为没有继承结构,所以不需要面向接口编程了),而deque就是Adaptee。stack/queue的底层是通过deque实现的。但在使用stack/queue的时候,客户端不需要知道底层是由deque实现的。
6. 相关模式
(1)适配器模式与桥接模式
①两者结构略为相似,但功能上完全不同
②适配器模式是把两个或多个接口的功能进行转换匹配;而桥接模式是让接口和实现部分相分离,以便它们可以相对独立的变化。
(2)适配器模式与装饰模式
①从某种意义上讲,适配器模式能模拟实现简单装饰模式的功能。在适配前和适配后都可以添加一些功能。
②仅仅是类似,造成这种类似的原因是:两种设计模式在实现上都是使用对象的组合,都可以在转调组合对象的功能前后进行一些附加的处理。
③两个模式最大的不同在于:适配器适配过后是需要改变接口的,而装饰模式是不改变接口的,无论多少装饰都是一个接口。
【编程实验】让数据库型的日志管理系统同时支持早期文本文件的管理方式
//结构型模式:适配器模式
//让支持数据库型的日志管理系统同时支持早期文本文件的管理方式
#include <iostream>
#include <string>
#include <list>
using namespace std;
//************************************日志数据对象********************************
//日志数据对象
class LogModel
{
string logId;//日志编号
string operateUser; //操作人员
string operateTime;//操作日期(YYYY-MM-DD HH:mm:ss格式)
string logContent; //日志内容
public:
string& getlogId()
{
return logId;
}
void setLogId(const string logId)
{
this->logId = logId;
}
string& getOperateUser()
{
return operateUser;
}
void setOperateUser(const string operateUser)
{
this->operateUser = operateUser;
}
string& getOperateTime()
{
return operateTime;
}
void setOperateTime(const string operateTime)
{
this->operateTime = operateTime;
}
string& getLogContent()
{
return logContent;
}
void setLogContent(const string logContent)
{
this->logContent = logContent;
}
string toString()
{
return "logId="+logId + ", "+
"operateUser=" + operateUser + ", "+
"operateTime=" + operateTime + ", "+
"logContent="+logContent;
}
};
//数据库操作提供的管理日志接口:4种(增、删、改、查)。
class LogDbOperateApi
{
public:
//增加日志对象
virtual void createLog(LogModel& lm) = 0;
//删除日志对象
virtual void removeLog(LogModel& lm) = 0;
//修改日志对象
virtual void updateLog(LogModel lm) = 0;
//查询所有日志对象
virtual void getAllLog() = 0;
};
//数据库方式的存取实现这里省略。。。
//******************************将日志存储在文件文件的API****************
//假设早期己经实现了将日志写入文本文件的功能,但当时的日志文件操作Api只有
//读取、写入和显示的3个功能。为了让我们这套数据库日志管理系统同时支持早期的文本文件
//存取功能,需要转换接口才能将现有的日志写入文本文件或从中读取出来。
//定义一个操作日志文件的接口:只有三个接口(读取、写入和显示操作)
class LogFileOperateApi
{
public:
//读取文件各个日志对象,并存储在日志列表中
virtual list<LogModel>& readLogFile() = 0;
//写入日志文件,把日志列表写出到日志文件中去
virtual void writeLogFile(list<LogModel>& list) = 0;
//显示出来
virtual void showLog() = 0;
};
//用文本文件对日志进行管理
class LogFileOperate: public LogFileOperateApi
{
list<LogModel> logList;
public:
list<LogModel>& readLogFile()
{
//这里省略了从文件中读取的过程,直接从保存在内存中的
//内容读取出来。
// list<LogModel>::iterator iter = logList.begin();
// while(iter != logList.end())
// {
// //cout << iter->toString() << endl;
// ++iter;
// }
return logList;
}
void writeLogFile(list<LogModel>& logList)
{
//这里省略了写入文件的过程,直接写到list中
this->logList.assign(logList.begin(),logList.end());
}
//显示出来
void showLog()
{
list<LogModel>::iterator iter = logList.begin();
while(iter != logList.end())
{
cout << iter->toString() << endl;
++iter;
}
}
~LogFileOperate()
{
logList.clear();
}
};
//*******************************************适配器*************************************
//适配器
class Adapter : public LogDbOperateApi
{
//持有需要被适配的接口对象
LogFileOperateApi* adaptee;
public:
//构造函数,传入需要被适配的对象
Adapter(LogFileOperateApi* adaptee)
{
this->adaptee = adaptee;
}
//增加日志对象
void createLog(LogModel& lm)
{
//1.先读取文件内容
list<LogModel>& logList = adaptee->readLogFile();
//2.加入新的日志对象
logList.push_back(lm);
//重新写入文件
adaptee->writeLogFile(logList);
}
//删除日志对象
void removeLog(LogModel& lm)
{
//1.先读取文件内容
list<LogModel>& logList = adaptee->readLogFile();
//2.删除日志对象
list<LogModel>::iterator iter = logList.begin();
while (iter != logList.end())
{
if( (*iter).getlogId() == lm.getlogId())
{
logList.erase(iter);
}
else
{
++iter;
}
}
//3.重新写入文件
adaptee->writeLogFile(logList);
}
//修改日志对象
void updateLog(LogModel lm)
{
//1.先读取文件内容
list<LogModel>& logList = adaptee->readLogFile();
//2.删除日志对象
list<LogModel>::iterator iter = logList.begin();
while (iter != logList.end())
{
if( (*iter).getlogId() == lm.getlogId())
{
iter->setOperateUser(lm.getOperateUser());
iter->setOperateTime(lm.getOperateTime());
iter->setLogContent(lm.getLogContent());
}
++iter;
}
//3.重新写入文件
adaptee->writeLogFile(logList);
}
//查询所有日志对象
void getAllLog()
{
adaptee-> showLog();
}
};
int main()
{
//准备日志内容,也就是测试的数据
LogModel lm1;
lm1.setLogId("001");
lm1.setOperateUser("admin");
lm1.setOperateTime("2016-05-21 16:48:00");
lm1.setLogContent("log1's content,just a test!");
LogModel lm2;
lm2.setLogId("002");
lm2.setOperateUser("user");
lm2.setOperateTime("2016-05-21 16:58:00");
lm2.setLogContent("log2's content,just a test!");
//创建操作日志文件的对象
LogFileOperateApi* logFileApi = new LogFileOperate();
//创建数据库操作日志的接口对象
LogDbOperateApi* api = new Adapter(logFileApi); //转换接口
//保存日志文件
api->createLog(lm1);
api->createLog(lm2);
//读取日志文件
api->getAllLog();
delete api;
delete logFileApi;
return 0;
}