【設計模式】 模式PK:策略模式VS橋梁模式
1、概述
我們先來看兩種模式的通用類圖。
兩者之間確實很相似。如果把策略模式的環境角色變更為一個抽象類加一個實現類,或者橋梁模式的抽象角色未實現,只有修正抽象化角色,想想看,這兩個類圖有什麽地方不一樣?完全一樣!正是由於類似場景的存在才導致了兩者在實際應用中經常混淆的情況發生,我們來舉例說明兩者有何差別。
大家都知道郵件有兩種格式:文本郵件(TextMail)和超文本郵件(HTMLMaiL),在文本郵件中只能有簡單的文字信息,而在超文本郵件中可以有復雜文字(帶有顏色、字體等屬性)、圖片、視頻等,如果你使用Foxmail郵件客戶端的話就應該有深刻體驗,看到一份郵件,怎麽沒內容?原來是你忘記點擊那個“HTML郵件”標簽了。下面我們就來講解如何發送這兩種不同格式的郵件,研究一下這兩種模式如何處理這樣的場景。
2、策略模式實現郵件發送
2.1 類圖
使用策略模式發送郵件,我們認為這兩種郵件是兩種不同的封裝格式,給定了發件人、收件人、標題、內容的一封郵件,按照兩種不同的格式分別進行封裝,然後發送之。按照這樣的分析,我們發現郵件的兩種不同封裝格式就是兩種不同的算法,具體到策略模式就是兩種不同策略,這樣看已經很簡單了,我們可以直接套用策略模式來實現。
我們定義了一個郵件模板,它有兩個實現類:TextMail(文本郵件)和HtmlMail(超文本郵件),分別實現兩種不同格式的郵件封裝。MailServer是一個環境角色,它接收一個MailTemplate對象,然後通過sendMail方法發送出去。
2.2 代碼
2.2.1 抽象郵件
class CMailTemplate { public: CMailTemplate(const string &sFrom, const string &sTo, const string &sSubject , const string &sContent) : msFrom(sFrom), msTo(sTo), msSubject(sSubject), msContent(sContent){} ~CMailTemplate(){};void mvSetFrom(const string &sFrom) { msFrom = sFrom; } string msGetFrom() { return msFrom; } void mvSetTo(const string &sTo) { msTo = sTo; } string msGetTo() { return msTo; } void mvSetSubject(const string &sSubject) { msSubject = sSubject; } string msGetSubject() { return msSubject; } void mvSetContent(const string &sContent) { msContent = sContent; } virtual string msGetContent(){ return msContent; } private: string msFrom; //郵件發件人 string msTo; //收件人 string msSubject; //郵件標題 string msContent; //通過構造函數傳遞郵件信息 };
抽象類沒有抽象的方法,設置為抽象類還有什麽意義呢?有意義,在這裏我們定義了一個這樣的抽象類:它具有郵件的所有屬性,但不是一個具體可以被實例化的對象。例如,你對郵件服務器說“給我制造一封郵件”,郵件服務器肯定拒絕,為什麽?你要產生什麽郵件?什麽格式的?郵件對郵件服務器來說是一個抽象表示,是一個可描述但不可形象化的事物。你可以這樣說:“我要一封標題為XX,發件人是XXX的文本格式的郵件”,這就是一個可實例化的對象,因此我們的設計就產生了兩個子類以具體化郵件,而且每種郵件格式對郵件的內容都有不同的處理。
2.2.2 文本郵件
class CTextMail : public CMailTemplate { public: CTextMail(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : CMailTemplate(sFrom, sTo, sSubject, sContent) {} ~CTextMail() {} string msGetContent() { //文本類型設置郵件的格式為: text/plain string s_content = "\nContent-Type: text/plain;charset=GB2312\n" + CMailTemplate::msGetContent(); //同時對郵件進行base64編碼處理,這裏用一句話代替 s_content += "\n郵件格式為: 文本格式"; return s_content; } };
我們覆寫了msGetContent方法,因為要把一封郵件設置為文本郵件必須加上一個特殊的標誌:text/plain,用於告訴解析這份郵件的客戶端:“我是一封文本格式的郵件,別解析錯了”。
2.2.3 超文本郵件
同樣,超文本格式的郵件也有類似的設置。
class CHtmlMail : public CMailTemplate { public: CHtmlMail(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : CMailTemplate(sFrom, sTo, sSubject, sContent) {} ~CHtmlMail() {} string msGetContent() { //超文本類型設置郵件的格式為: multipart/mixed string s_content = "\nContent-Type: multipart/mixed; charset= GB2312\n" + CMailTemplate::msGetContent(); //同時對郵件進行base64編碼處理,這裏用一句話代替 s_content += "\n郵件格式為: 超文本格式"; return s_content; } };
優秀一點的郵件客戶端會對郵件的格式進行檢查,比如編寫一封超文本格式的郵件,在內容中加上了<font>標簽,但是遺忘了</font>結尾標簽,郵件的產生者(也就是郵件的客戶端)會提示進行修正,我們這裏用了“郵件格式為:超文本格式”來代表該邏輯。兩個實現類實現了不同的算法,給定相同的發件人、收件人、標題和內容可以產生不同的郵件信息。
2.2.4 郵件服務器
class CMailServer { public: CMailServer(CMailTemplate *opMail) : mopMail(opMail) {} ~CMailServer(){} //發送郵件 void mvSendMail() { cout << "====正在發送的郵件信息====" << endl; //發件人 cout << "發件人: " << mopMail->msGetFrom().c_str() << endl; //收件人 cout << "收件人:" << mopMail->msGetTo().c_str() << endl; //標題 cout << "郵件標題: " << mopMail->msGetSubject().c_str() << endl; //郵件內容 cout << "郵件內容: " << mopMail->msGetContent().c_str() << endl; } private: //發送的是哪封郵件 CMailTemplate *mopMail; };
很簡單,郵件服務器接收了一封郵件,然後調用自己的發送程序進行發送。有人可能要問了,為什麽不把mvSendMail方法移植到郵件模板類中呢?這也是郵件模板類的一個行為,郵件可以被發送。是的,這確實是郵件的一個行為,完全可以這樣做,兩者沒有什麽區別,只是從不同的角度看待該方法而已。
2.2.5 場景調用
int main() { //創建一封TEXT格式的郵件 CMailTemplate *op_mail = new CHtmlMail("[email protected]", "[email protected]", "外星人攻擊地球了", "結果是外星人被地球人打敗了!"); //創建一個Mail發送程序 CMailServer *op_server = new CMailServer(op_mail); op_server->mvSendMail(); return 0; }
2.2.6 執行結果
當然,如果想產生一封文本格式的郵件,只要稍稍修改一下場景類就可以了:new CHtmlMail修改為new CTextMail,非常簡單。在該場景中,我們使用策略模式實現兩種算法的自由切換,它提供了這樣的保證:封裝郵件的兩種行為是可選擇的,至於選擇哪個算法是由上層模塊決定的。策略模式要完成的任務就是提供兩種可以替換的算法。
3、橋梁模式實現郵件發送
3.1 類圖
橋梁模式關註的是抽象和實現的分離,它是結構型模式,結構型模式研究的是如何建立一個軟件架構,下面我們就來看看橋梁模式是如何構件一套發送郵件的架構的。
類圖中我們增加了SendMail和Postfix兩個郵件服務器來實現類,在郵件模板中允許增加發送者標記,其他與策略模式都相同。我們在這裏已經完成了一個獨立的架構,郵件有了,發送郵件的服務器也具備了,是一個完整的郵件發送程序。需要註意的是,SendMail類不是一個動詞行為(發送郵件),它指的是一款開源郵件服務器產品,一般*nix系統的默認郵件服務器就是SendMail;Postfix也是一款開源的郵件服務器產品,其性能、穩定性都在逐步趕超SendMail。
3.2 代碼
3.2.1 郵件模板
我們來看代碼實現,郵件模板僅僅增加了一個add方法,文本郵件、超文本郵件都沒有任何改變。
//郵件模板 class CMailTemplate { public: CMailTemplate(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : msFrom(sFrom), msTo(sTo), msSubject(sSubject), msContent(sContent){} ~CMailTemplate(){}; void mvSetFrom(const string &sFrom) { msFrom = sFrom; } string msGetFrom() { return msFrom; } void mvSetTo(const string &sTo) { msTo = sTo; } string msGetTo() { return msTo; } void mvSetSubject(const string &sSubject) { msSubject = sSubject; } string msGetSubject() { return msSubject; } void mvSetContent(const string &sContent) { msContent = sContent; } virtual string msGetContent(){ return msContent; } //允許增加郵件發送標誌 void mvAdd(const string &sSendInfo) { msContent = sSendInfo + msContent; } private: string msFrom; //郵件發件人 string msTo; //收件人 string msSubject; //郵件標題 string msContent; //通過構造函數傳遞郵件信息 }; //文本郵件 class CTextMail : public CMailTemplate { public: CTextMail(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : CMailTemplate(sFrom, sTo, sSubject, sContent) {} ~CTextMail() {} string msGetContent() { //文本類型設置郵件的格式為: text/plain string s_content = "\nContent-Type: text/plain;charset=GB2312\n" + CMailTemplate::msGetContent(); //同時對郵件進行base64編碼處理,這裏用一句話代替 s_content += "\n郵件格式為: 文本格式"; return s_content; } }; //超文本郵件 class CHtmlMail : public CMailTemplate { public: CHtmlMail(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : CMailTemplate(sFrom, sTo, sSubject, sContent) {} ~CHtmlMail() {} string msGetContent() { //超文本類型設置郵件的格式為: multipart/mixed string s_content = "\nContent-Type: multipart/mixed; charset= GB2312\n" + CMailTemplate::msGetContent(); //同時對郵件進行base64編碼處理,這裏用一句話代替 s_content += "\n郵件格式為: 超文本格式"; return s_content; } };
3.2.2 郵件服務器
我們來看郵件服務器,也就是橋梁模式的抽象化角色。
class CMailServer { public: CMailServer(CMailTemplate *opMail) : mopMail(opMail) {}; ~CMailServer(){}; //發送郵件 virtual void mvSendMail() { cout << "====正在發送的郵件信息====" << endl; //發件人 cout << "發件人:" << mopMail->msGetFrom().c_str() << endl; //收件人 cout << "收件人:" << mopMail->msGetTo().c_str() << endl; //標題 cout << "郵件標題: " << mopMail->msGetSubject().c_str() << endl; //郵件內容 cout << "郵件內容: " << mopMail->msGetContent().c_str() << endl; } protected: //發送的是哪封郵件 CMailTemplate *mopMail; };
該類相對於策略模式的環境角色有兩個改變:
● 修改為抽象類。為什麽要修改成抽象類?因為我們在設計一個架構,郵件服務器是一個具體的、可實例化的對象嗎?“給我一臺郵件服務器”能實現嗎?不能,只能說“給我一臺Postfix郵件服務器”,這才能實現,必須有一個明確的可指向對象。
● 變量m修改為Protected訪問權限,方便子類調用。
3.2.3 Postfix郵件服務器
class CPostfix : public CMailServer { public: CPostfix(CMailTemplate *opMail) : CMailServer(opMail) {} ~CPostfix(){} //修正郵件發送程序 void mvSendMail() { //增加郵件服務器信息 string s_content = "Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com(Postfix) with ESMTP id 8DBCB1456B8\n"; mopMail->mvAdd(s_content); CMailServer::mvSendMail(); } };
3.2.4 SendMail郵件服務器
為什麽要覆寫mvSendMail程序呢?這是因為每個郵件服務器在發送郵件時都會在郵件內容上留下自己的標誌,一是廣告作用,二是為了互聯網上統計需要,三是方便同質軟件的共振。
class CSendMail : public CMailServer { public: CSendMail(CMailTemplate *opMail) : CMailServer(opMail) {} ~CSendMail(){} //修正郵件發送程序 void mvSendMail() { //增加郵件服務器信息 string s_content = "Received: (sendmail); 7 Nov 2009 04:14:44 +0100"; mopMail->mvAdd(s_content); CMailServer::mvSendMail(); } };
3.2.5 場景調用
郵件和郵件服務器都有了,我們來看怎麽發送郵件。
int main() { //創建一封TEXT格式的郵件 CMailTemplate *op_mail = new CTextMail("[email protected]", "[email protected]", "外星人攻擊地球了", " 結果地球人打敗了外星人!"); //使用Postfix發送郵件 CMailServer *op_post = new CPostfix(op_mail); //發送郵件 op_post->mvSendMail(); return 0; }
3.2.6 運行結果
當然了,還有其他三種發送郵件的方式:Postfix發送文本郵件以及SendMail發送文本郵件和超文本郵件。
4、總結
策略模式和橋梁模式是如此相似,我們只能從它們的意圖上來分析。策略模式是一個行為模式,旨在封裝一系列的行為,在例子中我們認為把郵件的必要信息(發件人、收件人、標題、內容)封裝成一個對象就是一個行為,封裝的格式(算法)不同,行為也就不同。而橋梁模式則是解決在不破壞封裝的情況下如何抽取出它的抽象部分和實現部分,它的前提是不破壞封裝,讓抽象部分和實現部分都可以獨立地變化,在例子中,我們的郵件服務器和郵件模板是不是都可以獨立地變化?不管是郵件服務器還是郵件模板,只要繼承了抽象類就可以繼續擴展,它的主旨是建立一個不破壞封裝性的可擴展架構。
簡單來說,策略模式是使用繼承和多態建立一套可以自由切換算法的模式,橋梁模式是在不破壞封裝的前提下解決抽象和實現都可以獨立擴展的模式。橋梁模式必然有兩個“橋墩”——抽象化角色和實現化角色,只要橋墩搭建好,橋就有了,而策略模式只有一個抽象角色,可以沒有實現,也可以有很多實現。
還是很難區分,是吧?多想想兩者的意圖,就可以理解為什麽要建立兩個相似的模式了。我們在做系統設計時,可以不考慮到底使用的是策略模式還是橋梁模式,只要好用,能夠解決問題就成,“不管黑貓白貓,抓住老鼠的就是好貓”。
【設計模式】 模式PK:策略模式VS橋梁模式