33.1 策略模式VS橋梁模式
這對冤家終于碰頭了,策略模式與橋梁模式是如此相似,簡直就是孿生兄弟,要把它們兩個分開可不太容易。我們來看看兩者的通用類圖,如圖33-1所示。

圖33-1 策略模式(左)和橋梁模式(右)通用類圖
兩者之間確實很相似。如果把策略模式的環境角色變更為一個抽象類加一個實現類,或者橋梁模式的抽象角色未實現,只有修正抽象化角色,想想看,這兩個類圖有什么地方不一樣?完全一樣!正是由于類似場景的存在才導致了兩者在實際應用中經常混淆的情況發生,我們來舉例說明兩者有何差別。
大家都知道郵件有兩種格式:文本郵件(Text Mail)和超文本郵件(HTML MaiL),在文本郵件中只能有簡單的文字信息,而在超文本郵件中可以有復雜文字(帶有顏色、字體等屬性)、圖片、視頻等,如果你使用Foxmail郵件客戶端的話就應該有深刻體驗,看到一份郵件,怎么沒內容?原來是你忘記點擊那個“HTML郵件”標簽了。下面我們就來講解如何發送這兩種不同格式的郵件,研究一下這兩種模式如何處理這樣的場景。
33.1.1 策略模式實現郵件發送
使用策略模式發送郵件,我們認為這兩種郵件是兩種不同的封裝格式,給定了發件人、收件人、標題、內容的一封郵件,按照兩種不同的格式分別進行封裝,然后發送之。按照這樣的分析,我們發現郵件的兩種不同封裝格式就是兩種不同的算法,具體到策略模式就是兩種不同策略,這樣看已經很簡單了,我們可以直接套用策略模式來實現。先看類圖,如圖33-2所示。

圖33-2 策略模式實現郵件發送的類圖
我們定義了一個郵件模板,它有兩個實現類:TextMail(文本郵件)和HtmlMail(超文本郵件),分別實現兩種不同格式的郵件封裝。MailServer是一個環境角色,它接收一個MailTemplate對象,然后通過sendMail方法發送出去。我們來看具體的代碼,先看抽象郵件,如代碼清單33-1所示。
代碼清單33-1 抽象郵件
public?abstract?class?MailTemplate?{
?????//郵件發件人
?????private?String?from;
?????//收件人
?????private?String?to;
?????//郵件標題
?????private?String?subject;
?????//郵件內容
?????private?String?context;
?????//通過構造函數傳遞郵件信息
?????public?MailTemplate(String?_from,String?_to,String?_subject,String?_context){
?????????????this.from?=?_from;
?????????????this.to?=?_to;
?????????????this.subject?=?_subject;
?????????????this.context?=?_context;
?????}
?????public?String?getFrom()?{
?????????????return?from;
?????}
?????public?void?setFrom(String?from)?{
?????????????this.from?=?from;
?????}
?????public?String?getTo()?{
?????????????return?to;
?????}
?????public?void?setTo(String?to)?{
?????????????this.to?=?to;
?????}
?????public?String?getSubject()?{
?????????????return?subject;
?????}
?????public?void?setSubject(String?subject)?{
?????????????this.subject?=?subject;
?????}
?????public?void?setContext(String?context){
?????????????this.context?=?context;
?????}
?????//郵件都有內容
?????public?String?getContext(){
?????????????return?context;
?????}
}
很奇怪,是嗎?抽象類沒有抽象的方法,設置為抽象類還有什么意義呢?有意義,在這里我們定義了一個這樣的抽象類:它具有郵件的所有屬性,但不是一個具體可以被實例化的對象。例如,你對郵件服務器說“給我制造一封郵件”,郵件服務器肯定拒絕,為什么?你要產生什么郵件?什么格式的?郵件對郵件服務器來說是一個抽象表示,是一個可描述但不可形象化的事物。你可以這樣說:“我要一封標題為XX,發件人是XXX的文本格式的郵件”,這就是一個可實例化的對象,因此我們的設計就產生了兩個子類以具體化郵件,而且每種郵件格式對郵件的內容都有不同的處理。我們首先看文本郵件,如代碼清單33-2所示。
代碼清單33-2 文本郵件
public?class?TextMail?extends?MailTemplate?{
?????public?TextMail(String?_from,?String?_to,?String?_subject,?String?_context)?{
?????????????super(_from,?_to,?_subject,?_context);
?????}
?????public?String?getContext()?{
?????????????//文本類型設置郵件的格式為:text/plain
?????????????String?context?=?"\nContent-Type:?text/plain;charset=GB2312\n"?+super.getContext();
?????????????//同時對郵件進行base64編碼處理,這里用一句話代替
?????????????context?=?context?+?"\n郵件格式為:文本格式";
?????????????return?context;
?????}
}
我們覆寫了getContext方法,因為要把一封郵件設置為文本郵件必須加上一個特殊的標志:text/plain,用于告訴解析這份郵件的客戶端:“我是一封文本格式的郵件,別解析錯了”。同樣,超文本格式的郵件也有類似的設置,如代碼清單33-3所示。
代碼清單33-3 超文本郵件
public?class?HtmlMail?extends?MailTemplate?{
?????public?HtmlMail(String?_from,?String?_to,?String?_subject,?String?_context)?{
?????????????super(_from,?_to,?_subject,?_context);
?????}
?????public?String?getContext(){
?????????????//超文本類型設置郵件的格式為:multipart/mixed
?????????????String?context?=?"\nContent-Type:?multipart/mixed;?charset=?GB2312\n"?+super.getContext();
?????????????//同時對郵件進行HTML檢查,是否有類似未關閉的標簽
?????????????context?=?context?+?"\n郵件格式為:超文本格式";
?????????????return?context;
?????}
}
優秀一點的郵件客戶端會對郵件的格式進行檢查,比如編寫一封超文本格式的郵件,在內容中加上了<font>標簽,但是遺忘了</font>結尾標簽,郵件的產生者(也就是郵件的客戶端)會提示進行修正,我們這里用了“郵件格式為:超文本格式”來代表該邏輯。
兩個實現類實現了不同的算法,給定相同的發件人、收件人、標題和內容可以產生不同的郵件信息。我們看看郵件是如何發送出去的,如代碼清單33-4所示。
代碼清單33-4 郵件服務器
public?class?MailServer?{
?????//發送的是哪封郵件
?????private?MailTemplate?m;
?????public?MailServer(MailTemplate?_m){
?????????????this.m??=?_m;
?????}
?????//發送郵件
?????public?void?sendMail(){
?????????????System.out.println("====正在發送的郵件信息====");
?????????????//發件人
?????????????System.out.println("發件人:"?+?m.getFrom());
?????????????//收件人
?????????????System.out.println("收件人:"?+?m.getTo());
?????????????//標題
?????????????System.out.println("郵件標題:"?+?m.getSubject());
?????????????//郵件內容
?????????????System.out.println("郵件內容:"?+?m.getContext());
?????}
}
很簡單,郵件服務器接收了一封郵件,然后調用自己的發送程序進行發送。可能讀者要問了,為什么不把sendMail方法移植到郵件模板類中呢?這也是郵件模板類的一個行為,郵件可以被發送。是的,這確實是郵件的一個行為,完全可以這樣做,兩者沒有什么區別,只是從不同的角度看待該方法而已。我們繼續看場景類,如代碼清單33-5所示。
代碼清單33-5 場景類
public?class?Client?{
?????public?static?void?main(String[]?args)?{
?????????????//創建一封TEXT格式的郵件
?????????????MailTemplate?m?=?new?HtmlMail("a@a.com","b@b.com","外星人攻擊地球了","結局是外星人被地球人打敗了!");
?????????????//創建一個Mail發送程序
?????????????MailServer?mail?=?new?MailServer(m);
?????????????//發送郵件
?????????????mail.sendMail();
?????}
}
運行結果如下所示:
====正在發送的郵件信息====
發件人:a@a.com
收件人:b@b.com
郵件標題:外星人攻擊地球了
郵件內容:
Content-Type: multipart/mixed;charset=GB2312
結局是外星人被地球人打敗了!
郵件格式為:超文本格式
當然,如果想產生一封文本格式的郵件,只要稍稍修改一下場景類就可以了:new HtmlMail修改為new TextMail,讀者可以自行實現,非常簡單。在該場景中,我們使用策略模式實現兩種算法的自由切換,它提供了這樣的保證:封裝郵件的兩種行為是可選擇的,至于選擇哪個算法是由上層模塊決定的。策略模式要完成的任務就是提供兩種可以替換的算法。
33.1.2 橋梁模式實現郵件發送
橋梁模式關注的是抽象和實現的分離,它是結構型模式,結構型模式研究的是如何建立一個軟件架構,下面我們就來看看橋梁模式是如何構件一套發送郵件的架構的,如圖33-3所示。
類圖中我們增加了SendMail和Postfix兩個郵件服務器來實現類,在郵件模板中允許增加發送者標記,其他與策略模式都相同。我們在這里已經完成了一個獨立的架構,郵件有了,發送郵件的服務器也具備了,是一個完整的郵件發送程序。需要讀者注意的是,SendMail類不是一個動詞行為(發送郵件),它指的是一款開源郵件服務器產品,一般*nix系統的默認郵件服務器就是SendMail;Postfix也是一款開源的郵件服務器產品,其性能、穩定性都在逐步趕超SendMail。

圖33-3 橋梁模式實現郵件發送的類圖
我們來看代碼實現,郵件模板僅僅增加了一個add方法,如代碼清單33-6所示。
代碼清單33-6 郵件模板
public?abstract?class?MailTemplate?{
?????/*
?????*該部分代碼不變,請參考代碼清單33-1
?????*/?????
?????//允許增加郵件發送標志
?????public?void?add(String?sendInfo){
?????????????context?=?sendInfo?+?context;
?????}
}
文本郵件、超文本郵件都沒有任何改變,如代碼清單33-2、33-3所示,這里不再贅述。
我們來看郵件服務器,也就是橋梁模式的抽象化角色,如代碼清單33-7所示。
代碼清單33-7 郵件服務器
public?abstract?class?MailServer?{
?????//發送的是哪封郵件
?????protected?final?MailTemplate?m;
?????public?MailServer(MailTemplate?_m){
?????????????this.m??=?_m;
?????}
?????//發送郵件
?????public?void?sendMail(){
?????????????System.out.println("====正在發送的郵件信息====");
?????????????//發件人
?????????????System.out.println("發件人:"?+?m.getFrom());
?????????????//收件人
?????????????System.out.println("收件人:"?+?m.getTo());
?????????????//標題
?????????????System.out.println("郵件標題:"?+?m.getSubject());
?????????????//郵件內容
?????????????System.out.println("郵件內容:"?+?m.getContext());
?????}
}
該類相對于策略模式的環境角色有兩個改變:
● 修改為抽象類。為什么要修改成抽象類?因為我們在設計一個架構,郵件服務器是一個具體的、可實例化的對象嗎?“給我一臺郵件服務器”能實現嗎?不能,只能說“給我一臺Postfix郵件服務器”,這才能實現,必須有一個明確的可指向對象。
● 變量m修改為Protected訪問權限,方便子類調用。
我們再來看看Postfix郵件服務器的實現,如代碼清單33-8所示。
代碼清單33-8 Postfix郵件服務器
public?class?Postfix?extends?MailServer?{
?????public?Postfix(MailTemplate?_m)?{
?????????????super(_m);
?????}
?????//修正郵件發送程序
?????public?void?sendMail(){
?????????????????????//增加郵件服務器信息
?????????????String?context?="Received:?from?XXXX?(unknown?[xxx.xxx.xxx.xxx])?by?aaa.aaa.com?(Postfix)?with?ESMTP?id?8DBCD172B8\n"?;
?????????????super.m.add(context);
?????????????super.sendMail();
?????}
}
為什么要覆寫sendMail程序呢?這是因為每個郵件服務器在發送郵件時都會在郵件內容上留下自己的標志,一是廣告作用,二是為了互聯網上統計需要,三是方便同質軟件的共振。我們再來看SendMail郵件服務器的實現,如代碼清單33-9所示。
代碼清單33-9 SendMail郵件服務器
public?class?SendMail?extends?MailServer?{
?????//傳遞一封郵件
?????public?SendMail(MailTemplate?_m)?{
?????????????super(_m);
?????}
?????//修正郵件發送程序
?????@Override
?????public?void?sendMail(){
?????????????//增加郵件服務器信息
?????????????super.m.add("Received:?(sendmail);?7?Nov?2009?04:14:44?+0100");
?????????????super.sendMail();
?????}
}
郵件和郵件服務器都有了,我們來看怎么發送郵件,如代碼清單33-10所示。
代碼清單33-10 場景類
public?class?Client?{
?????public?static?void?main(String[]?args)?{
?????????????//創建一封TEXT格式的郵件
?????????????MailTemplate?m?=?new?HtmlMail("a@a.com","b@b.com","外星人攻擊地球了","?結局是外星人被地球人打敗了!");
?????????????//使用Postfix發送郵件
?????????????MailServer?mail?=?new?Postfix(m);
?????????????//發送郵件
?????????????mail.sendMail();
?????}
}
運行結果如下所示:
====正在發送的郵件信息====
發件人:a@a.com
收件人:b@b.com
郵件標題:外星人攻擊地球了
郵件內容:
Content-Type: multipart/mixed;charset=GB2312
Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com (Postfix) with ESMTP id 8DBCD172B8
結局是外星人被地球人打敗了!
郵件格式為:超文本格式
當然了,還有其他三種發送郵件的方式:Postfix發送文本郵件以及SendMail發送文本郵件和超文本郵件。修改量很小,讀者可以自行修改實現,體會一下橋梁模式的特點。
33.1.3 最佳實踐
策略模式和橋梁模式是如此相似,我們只能從它們的意圖上來分析。策略模式是一個行為模式,旨在封裝一系列的行為,在例子中我們認為把郵件的必要信息(發件人、收件人、標題、內容)封裝成一個對象就是一個行為,封裝的格式(算法)不同,行為也就不同。而橋梁模式則是解決在不破壞封裝的情況下如何抽取出它的抽象部分和實現部分,它的前提是不破壞封裝,讓抽象部分和實現部分都可以獨立地變化,在例子中,我們的郵件服務器和郵件模板是不是都可以獨立地變化?不管是郵件服務器還是郵件模板,只要繼承了抽象類就可以繼續擴展,它的主旨是建立一個不破壞封裝性的可擴展架構。
簡單來說,策略模式是使用繼承和多態建立一套可以自由切換算法的模式,橋梁模式是在不破壞封裝的前提下解決抽象和實現都可以獨立擴展的模式。橋梁模式必然有兩個“橋墩”——抽象化角色和實現化角色,只要橋墩搭建好,橋就有了,而策略模式只有一個抽象角色,可以沒有實現,也可以有很多實現。
還是很難區分,是吧?多想想兩者的意圖,就可以理解為什么要建立兩個相似的模式了。我們在做系統設計時,可以不考慮到底使用的是策略模式還是橋梁模式,只要好用,能夠解決問題就成,“不管黑貓白貓,抓住老鼠的就是好貓”。
- 前言
- 第一部分 大旗不揮,誰敢沖鋒——6大設計原則全新解讀
- 第1章 單一職責原則
- 1.2 絕殺技,打破你的傳統思維
- 1.3 我單純,所以我快樂
- 1.4 最佳實踐
- 第2章 里氏替換原則
- 2.2 糾紛不斷,規則壓制
- 2.3 最佳實踐
- 第3章 依賴倒置原則
- 3.2 言而無信,你太需要契約
- 3.3 依賴的三種寫法
- 3.4 最佳實踐
- 第4章 接口隔離原則
- 4.2 美女何其多,觀點各不同
- 4.3 保證接口的純潔性
- 4.4 最佳實踐
- 第5章 迪米特法則
- 5.2 我的知識你知道得越少越好
- 5.3 最佳實踐
- 第6章 開閉原則
- 6.2 開閉原則的廬山真面目
- 6.3 為什么要采用開閉原則
- 6.4 如何使用開閉原則
- 6.5 最佳實踐
- 第二部分 真刀實槍 ——23種設計模式完美演繹
- 第7章 單例模式
- 7.2 單例模式的定義
- 7.3 單例模式的應用
- 7.4 單例模式的擴展
- 7.5 最佳實踐
- 第8章 工廠方法模式
- 8.2 工廠方法模式的定義
- 8.3 工廠方法模式的應用
- 8.4 工廠方法模式的擴展
- 8.5 最佳實踐
- 第9章 抽象工廠模式
- 9.2 抽象工廠模式的定義
- 9.3 抽象工廠模式的應用
- 9.4 最佳實踐
- 第10章 模板方法模式
- 10.2 模板方法模式的定義
- 10.3 模板方法模式的應用
- 10.4 模板方法模式的擴展
- 10.5 最佳實踐
- 第11章 建造者模式
- 11.2 建造者模式的定義
- 11.3 建造者模式的應用
- 11.4 建造者模式的擴展
- 11.5 最佳實踐
- 第12章 代理模式
- 12.2 代理模式的定義
- 12.3 代理模式的應用
- 12.4 代理模式的擴展
- 12.5 最佳實踐
- 第13章 原型模式
- 13.2 原型模式的定義
- 13.3 原型模式的應用
- 13.4 原型模式的注意事項
- 13.5 最佳實踐
- 第14章 中介者模式
- 14.2 中介者模式的定義
- 14.3 中介者模式的應用
- 14.4 中介者模式的實際應用
- 14.5 最佳實踐
- 第15章 命令模式
- 15.2 命令模式的定義
- 15.3 命令模式的應用
- 15.4 命令模式的擴展
- 15.5 最佳實踐
- 第16章 責任鏈模式
- 16.2 責任鏈模式的定義
- 16.3 責任鏈模式的應用
- 16.4 最佳實踐
- 第17章 裝飾模式
- 17.2 裝飾模式的定義
- 17.3 裝飾模式應用
- 17.4 最佳實踐
- 第18章 策略模式
- 18.2 策略模式的定義
- 18.3 策略模式的應用
- 18.4 策略模式的擴展
- 18.5 最佳實踐
- 第19章 適配器模式
- 19.2 適配器模式的定義
- 19.3 適配器模式的應用
- 19.4 適配器模式的擴展
- 19.5 最佳實踐
- 第20章 迭代器模式
- 20.2 迭代器模式的定義
- 20.3 迭代器模式的應用
- 20.4 最佳實踐
- 第21章 組合模式
- 21.2 組合模式的定義
- 21.3 組合模式的應用
- 21.4 組合模式的擴展
- 21.5 最佳實踐
- 第22章 觀察者模式
- 22.2 觀察者模式的定義
- 22.3 觀察者模式的應用
- 22.4 觀察者模式的擴展
- 22.5 最佳實踐
- 第23章 門面模式
- 23.2 門面模式的定義
- 23.3 門面模式的應用
- 23.4 門面模式的注意事項
- 23.5 最佳實踐
- 第24章 備忘錄模式
- 24.2 備忘錄模式的定義
- 24.3 備忘錄模式的應用
- 24.4 備忘錄模式的擴展
- 24.5 最佳實踐
- 第25章 訪問者模式
- 25.2 訪問者模式的定義
- 25.3 訪問者模式的應用
- 25.4 訪問者模式的擴展
- 25.5 最佳實踐
- 第26章 狀態模式
- 26.2 狀態模式的定義
- 26.3 狀態模式的應用
- 第27章 解釋器模式
- 27.2 解釋器模式的定義
- 27.3 解釋器模式的應用
- 27.4 最佳實踐
- 第28章 享元模式
- 28.2 享元模式的定義
- 28.3 享元模式的應用
- 28.4 享元模式的擴展
- 28.5 最佳實踐
- 第29章 橋梁模式
- 29.2 橋梁模式的定義
- 29.3 橋梁模式的應用
- 29.4 最佳實踐
- 第三部分 誰的地盤誰做主 ——設計模式PK
- 第30章 創建類模式大PK
- 30.1 工廠方法模式VS建造者模式
- 30.2 抽象工廠模式VS建造者模式
- 第31章 結構類模式大PK
- 31.1 代理模式VS裝飾模式
- 31.2 裝飾模式VS適配器模式
- 第32章 行為類模式大PK
- 32.1 命令模式VS策略模式
- 32.2 策略模式VS狀態模式
- 32.3 觀察者模式VS責任鏈模式
- 第33章 跨戰區PK
- 33.1 策略模式VS橋梁模式
- 33.2 門面模式VS中介者模式
- 33.3 包裝模式群PK
- 第四部分 完美世界 ——設計模式混編
- 第34章 命令模式+責任鏈模式
- 34.2 混編小結
- 第35章 工廠方法模式+策略模式
- 35.2 混編小結
- 第36章 觀察者模式+中介者模式
- 36.2 混編小結
- 第五部分 擴展篇
- 第37章 MVC框架
- 37.2 最佳實踐
- 第38章 新模式
- 38.1 規格模式
- 38.2 對象池模式
- 38.3 雇工模式
- 38.4 黑板模式
- 38.5 空對象模式
- 附錄 23種設計模式彩圖