實(shí)用指南:C++設計模式_結構型模式_適配器模式Adapter
從本篇開(kāi)始記錄結構型模式。
結構型模式,該模式關(guān)注對象之間的組合關(guān)系,旨在解決如何構建靈活且可復用的類(lèi)和對象結構。共七種:適配器模式、裝飾器模式、代理模式、外觀(guān)模式、橋接模式、組合模式、享元模式。
適配器(Adapter)模式是一種結構型模式。生活中有很多適配器思想的實(shí)際應用,例如,各種轉接頭(usb轉接頭、hdmi轉接頭)、電源適配器(變壓器)等,主要是用來(lái)解決不同的接口之間的兼容問(wèn)題。設想一下,220V的市電不能直接給手機充電,引人一個(gè)電源適配器,把220V的市電轉換為5V的電壓,就可以給手機充電了。在軟件開(kāi)發(fā)領(lǐng)域,兩個(gè)類(lèi)之間也存在這種不兼容的問(wèn)題,于是,就可以像生活中引人電源適配器那樣引入一個(gè)適配器角色的類(lèi)來(lái)解決這兩個(gè)類(lèi)之間的兼容性問(wèn)題。
一個(gè)簡(jiǎn)單的例子
下面是一個(gè)簡(jiǎn)單的示例,這個(gè)類(lèi)中只有日志的一些基礎操作函數。
class LogToFile
{
public:
void initfile()
{
//做日志文件初始化工作,比如打開(kāi)文件等等
//.....
cout << "日志初始化工作完成" << endl;
}
void writetofile(const char* pcontent)
{
//將日志內容寫(xiě)入文件
//......
cout << "pcontent 寫(xiě)入到了文件中" << endl;
}
void readfromfile()
{
//從日志中讀取一些信息
//......
cout << "從文件中讀取了一些信息" << endl;
}
void closefile()
{
//關(guān)閉日志文件
//......
cout << "關(guān)閉日志文件" << endl;
}
// ......
};
void test()
{
LogToFile logtofile;
logtofile.initfile();
logtofile.writetofile("hello");
logtofile.readfromfile();
logtofile.closefile();
/*
日志初始化工作完成
pcontent 寫(xiě)入到了文件中
從文件中讀取了一些信息
關(guān)閉日志文件
*/
}
隨著(zhù)項目規模的不斷增加,要記錄的日志信息也逐漸增多,單純地向日志文件中記錄日志信息會(huì )導致日志文件膨脹得過(guò)大,不方便管理和查看。于是準備對項目中的日志系統進(jìn)行升級改造,即從原有的將日志信息寫(xiě)入文件改為將日志信息寫(xiě)人到數據庫。改造后的新日志系統如下:
class LogToDatabase
{
public:
void initdb()
{
//連接數據庫,做一些基本的數據庫連接設置等
//......
cout << "連接數據庫" << endl;
}
void writetodb(const char* pcontent)
{
//將日志內容寫(xiě)入數據庫
//......
cout << "pcontent 寫(xiě)入到了數據庫中" << endl;
}
void readfromdb()
{
//從數據庫中讀取一些日志信息
//......
cout << "從數據庫中讀取了一些日志信息" << endl;
}
void closedb()
{
//關(guān)閉到數據庫的鏈接
//......
cout << "關(guān)閉到數據庫的鏈接" << endl;
}
};
void test2()
{
LogToDatabase logtodatabase;
logtodatabase.initdb();
logtodatabase.writetodb("hello");
logtodatabase.readfromdb();
logtodatabase.closedb();
/*
連接數據庫
pcontent 寫(xiě)入到了數據庫中
從數據庫中讀取了一些日志信息
關(guān)閉到數據庫的鏈接
*/
}
現在遇到一個(gè)問(wèn)題,數據庫斷電或者電纜出現問(wèn)題,導致了新的日志系統不能使用了??梢允褂肔ogToFile類(lèi)來(lái)解決上面問(wèn)題,但是現在項目中所有的位置都使用的是 LogToDatabase類(lèi)的接口,而LogToFile類(lèi)接口與LogToDatabase接口又不完全相同,該怎么辦呢?現在的解決辦法有三種:
1 將所有的接口改為L(cháng)ogToFile類(lèi)的接口,這種改動(dòng)較大;
2 在LogToDataBase類(lèi)中增加新接口來(lái)支持對日志文件的讀寫(xiě),也就是把LogToFile 中的接口,在LogToDataBase 中重新實(shí)現一遍,這比較麻煩。
3 使用適配器解決。在該模式中,通過(guò)引人適配器類(lèi),把LogToDatabase類(lèi)中,諸如對 writeToDb、readfromDb等成員函數的調用轉換為對LogToFie類(lèi)中,諸如對 WriteToFile readfromFile等成員函數的調用, 從而實(shí)現接口調用的目的。
適配器方式如下:
class LogToFile
{
public:
void initfile()
{
//做日志文件初始化工作,比如打開(kāi)文件等等
//.....
cout << "日志初始化工作完成" << endl;
}
void writetofile(const char* pcontent)
{
//將日志內容寫(xiě)入文件
//......
cout << "pcontent 寫(xiě)入到了文件中" << endl;
}
void readfromfile()
{
//從日志中讀取一些信息
//......
cout << "從文件中讀取了一些信息" << endl;
}
void closefile()
{
//關(guān)閉日志文件
//......
cout << "關(guān)閉日志文件" << 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()
{
}
};
// 類(lèi)適配器
class LogAdapter : public LogToDatabase
{
public:
//構造函數
LogAdapter(LogToFile* pfile) //形參是老接口所屬類(lèi)的指針
{
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; // 聚合關(guān)系
};
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 寫(xiě)入到了文件中
在LogAdapter::readfromdb()中適配LogToFile::readfromfile()
從文件中讀取了一些信息
在LogAdapter::closedb()中適配LogToFile::closefile()
關(guān)閉日志文件
*/
}
通過(guò)代碼可以看到,在test中的代碼僅僅做了很小的變動(dòng),其中對接口,例如initdb、writetodb、readfromdb、closedb后調用更是沒(méi)有發(fā)生改動(dòng),通過(guò)引人適配器類(lèi),實(shí)際調用的接口是 LogToFile 類(lèi)的 initfile,writetofile、readfromfile、closefile。
實(shí)際上,適配器的能力簡(jiǎn)而言之就是能夠將對一種接口的調用轉為對另一種接口的調用。使用適配器轉換兩個(gè)類(lèi)時(shí),這兩個(gè)類(lèi)必須是相關(guān)的兩個(gè)類(lèi)。
引入適配器(Adapter)模式
上面的代碼完成了新老日志系統的轉換。在不改變老日志系統源碼的情況下,通過(guò)引人適配器,將使用新日志系統的項目與日志系統連接起來(lái),此時(shí),適配器扮演一個(gè)中間人的角色,將項目中針對日志系統的接口轉換成對應的老日志系統的接口調用,從而達到新接口適配老接的目的,這就是適配模式的工作。
兩個(gè)獨立的日志系統;
使用適配器連接起來(lái)。
適配器的定義:將一個(gè)類(lèi)的接口轉換成客戶(hù)希望的另外一個(gè)接口。該模式使得原本因為接口不兼容而不能一起工作的類(lèi)可以一起工作(在上述范例中,不一起工作的類(lèi)指的就是 LogToDatabase和LogToFile類(lèi))。適配器模式還有一個(gè)別名叫作包裝器(Wrapper)。
適配器的UML圖:
【上圖中,LogAdaper和LogToFile之間是聚合關(guān)系,因為在LogAdaper中保存了LogToFile的指針】
適配器模式中包含3種角色:
1 Target(日標抽象類(lèi)):該類(lèi)定義所需要暴露的接口(諸如initdb、writetodbteadfromdb、closedb等)。
2 Adaptee(適配者類(lèi)): 該類(lèi)扮演著(zhù)被適配的角色,其中定義了一個(gè)或多個(gè)已經(jīng)存在,的接口(老接口),這些接口需要適配(對其他接口的調用轉換成對這些接口的調用)。
3 Adapter(適配器類(lèi)): 注意英文字母的拼寫(xiě)區別于A(yíng)daptee(適配者類(lèi))。適配器類(lèi)是一個(gè)包裝類(lèi),扮演著(zhù)轉換器的角色,是適配器模式的實(shí)現核心,用于調用另一個(gè)接口(包裝適配者)。
適配器模式與裝飾模式有類(lèi)似的地方,兩者都使用了類(lèi)與類(lèi)之間的組合關(guān)系,但兩者實(shí)現意圖是不同的,適配器模式是將原有的接口適配成另外一個(gè)接口,而裝飾模式是對原有功能的增強,而且無(wú)論裝飾多少層,裝飾模式的調用接口始終不發(fā)生改變。
類(lèi)適配器
適配器模式依據實(shí)現方式分為兩種:一種是對象適配器,另一種是類(lèi)適配器。前邊是對象適配器,這種實(shí)現方式使用的是類(lèi)與類(lèi)之間的聚合關(guān)系,實(shí)現了委托機制。LogAdapter類(lèi)中包含了LogToFile指針, 可以認為 LogToFile是LogAdapter的一部分。類(lèi)適配器是通過(guò)繼承方式來(lái)實(shí)現接口的適配:
class LogToFile
{
public:
void initfile()
{
//做日志文件初始化工作,比如打開(kāi)文件等等
//.....
cout << "日志初始化工作完成" << endl;
}
void writetofile(const char* pcontent)
{
//將日志內容寫(xiě)入文件
//......
cout << "pcontent 寫(xiě)入到了文件中" << endl;
}
void readfromfile()
{
//從日志中讀取一些信息
//......
cout << "從文件中讀取了一些信息" << endl;
}
void closefile()
{
//關(guān)閉日志文件
//......
cout << "關(guān)閉日志文件" << 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 繼承,父類(lèi)的public protected成員函數在子類(lèi)中變成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 寫(xiě)入到了文件中
在LogAdapter::readfromdb()中適配LogToFile::readfromfile()
從文件中讀取了一些信息
在LogAdapter::closedb()中適配LogToFile::closefile()
關(guān)閉日志文件
*/
}

從代碼中可以看到,LogAdapter使用了多重繼承,以public(公有繼承)的方式繼承
LogToDatabase, public繼承所作表的是一種is-a關(guān)系,也就是通過(guò)子類(lèi)產(chǎn)生的對象一定也是一個(gè)父類(lèi)對象(子類(lèi)繼承了父類(lèi)的接口)。同時(shí),LogAdapter 還以 private(protected 也可以)的方式繼承了LogToFile類(lèi), private繼承關(guān)系就不是一種is-a關(guān)系了,而是一種組合關(guān)系,更明確地說(shuō),是組合關(guān)系中的:implemented-in-terms-of(根據……實(shí)現出)關(guān)系,這里的private繼承就表示想通過(guò)LogToFile類(lèi)實(shí)現出LogAdapter的意思。
適配器模式的擴展運用
使用適配器模式并不一定是好事,而是在開(kāi)發(fā)后期不得以才使用這種設計模式。但軟件開(kāi)發(fā)中也存在時(shí)常要發(fā)布新版本的情況,新版本也存在與老版本的兼容性問(wèn)題,有時(shí)完全拋棄老版本并不現實(shí),所以才借助適配器模式使新老版本兼容。在遺留代碼的復用、類(lèi)庫的遷移等工作方面,適配器模式仍舊能發(fā)揮巨大的作用。
C++標準庫中大量使用了適配器,容器適配器,算法適配器,迭代器適配器等。
(1) 容器適配器: 對于容器中的雙端隊列deque,它既包含了堆棧stack的能力也包含了隊列queue的能力,在實(shí)現stack和queue 源碼時(shí),只需要利用既有的 deque 源碼并進(jìn)行適當的改造(減少一些東西)。因此stack和queue都可以看作容器適配器。
_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(綁定器)就是一個(gè)典型的算法適配器;
(3)迭代器適配器:例如reverse_iterator(反向迭代器),其實(shí)現只是對迭代器iterator的一層簡(jiǎn)單封裝。
