第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;
}