結(jié)構(gòu)型設(shè)計(jì)模式(上)本教程主要介紹一系列用于如何將現(xiàn)有類或?qū)ο蠼M合在一起形成更加強(qiáng)大結(jié)構(gòu)的經(jīng)驗(yàn)總結(jié),。
知識(shí)結(jié)構(gòu):
圖1 知識(shí)結(jié)構(gòu) 享元模式 -- 實(shí)現(xiàn)對(duì)象的復(fù)用Sunny 軟件公司欲開發(fā)一個(gè)圍棋軟件,,其界面效果如下圖所示:
圖2 圍棋軟件界面效果圖
Sunny 軟件公司開發(fā)人員通過對(duì)圍棋軟件進(jìn)行分析,發(fā)現(xiàn)在圍棋棋盤中包含大量的黑子和白子,,它們的形狀,、大小都一模一樣,只是出現(xiàn)的位置不同而已,。
如果將每一個(gè)棋子都作為一個(gè)獨(dú)立的對(duì)象存儲(chǔ)在內(nèi)存中,將導(dǎo)致圍棋軟件在運(yùn)行時(shí)所需內(nèi)存空間較大,,如何降低運(yùn)行代價(jià),、提高系統(tǒng)性能是 Sunny 公司開發(fā)人員需要解決的一個(gè)問題 。
為了節(jié)約存儲(chǔ)空間,,提高系統(tǒng)性能,Sunny 公司開發(fā)人員使用共享技術(shù)來設(shè)計(jì)圍棋軟件中的棋子,,其基本結(jié)構(gòu)如下圖所示:
圖3 圍棋棋子結(jié)構(gòu)圖
Chessman
充當(dāng)抽象享元類,,WhiteChessman
和BlackChessman
是具體享元類,,ChessmanFactory
是享元工廠類,。
在實(shí)現(xiàn)該類時(shí),,使用了 單列模式 和 簡(jiǎn)單工廠模式 ,,確保了工廠對(duì)象的唯一性,并提供工廠方法來向客戶端返回共享對(duì)象,。
將棋子的位置Coordinates
定義為棋子的一個(gè)外部狀態(tài),在需要時(shí)再進(jìn)行設(shè)置,,這樣即解決了黑子和白子的共享,又解決了顯示不同位置的問題,。
完整代碼如下:
(1)棋子位置類(坐標(biāo)類):
public class Coordinates { public int X { get; set ; } public int Y { get; set ; } public Coordinates (int x, int y) { X = x; Y = y; } public override string ToString () { return X + "," + Y; } }
(2)棋子類:
//棋子抽象類 public abstract class Chessman { public abstract string Color { get; } public void Display (Coordinates coord) { Console.WriteLine("棋子顏色:{0},位置:{1}" , Color, coord); } }//白色棋子實(shí)體類 public class WhiteChessman : Chessman { public override string Color { get { return "白色" ; } } }//黑色棋子實(shí)體類 public class BlackChessman : Chessman { public override string Color { get { return "黑色" ; } } }
(3)圍棋棋子工廠類:
public class ChessmanFactory { private static readonly ChessmanFactory Instance = new ChessmanFactory(); private static Hashtable _ht; //使用HashTable來存儲(chǔ)共享對(duì)象 private ChessmanFactory () { _ht = new Hashtable(); Chessman black = new BlackChessman(); Chessman white = new WhiteChessman(); _ht.Add("b" , black); _ht.Add("w" , white); } //返回共享工廠的唯一實(shí)例 public static ChessmanFactory GetInstance () { return Instance; } //通過key獲取存儲(chǔ)在HashTable中的共享對(duì)象 public Chessman GetChessman (string color) { return _ht[color] as Chessman; } }
(4)客戶端:
static void Main (string [] args) { ChessmanFactory factory = ChessmanFactory.GetInstance(); Chessman black1 = factory.GetChessman("b" ); Chessman black2 = factory.GetChessman("b" ); Chessman black3 = factory.GetChessman("b" ); Console.WriteLine("判斷兩顆黑子是否相同:{0}" , object.ReferenceEquals(black1, black2)); Chessman white1 = factory.GetChessman("w" ); Chessman white2 = factory.GetChessman("w" ); Console.WriteLine("判斷兩顆白子是否相同:{0}" , object.ReferenceEquals(white1, white2)); black1.Display(new Coordinates(1 , 2 )); black2.Display(new Coordinates(3 , 4 )); black3.Display(new Coordinates(1 , 3 )); white1.Display(new Coordinates(2 , 5 )); white2.Display(new Coordinates(2 , 4 )); }
輸出結(jié)果如下圖所示:
圖4 運(yùn)行結(jié)果
從輸出結(jié)果可以看出,,雖然我們獲取了三個(gè)黑子對(duì)象和兩個(gè)白子對(duì)象,,但是它們的內(nèi)存地址相同,,也就是說,它們實(shí)際上是同一個(gè)對(duì)象,。
在每次調(diào)用display()
方法時(shí),,由于設(shè)置了不同的外部狀態(tài),所以顯示在棋盤的不同位置,。
若一個(gè)軟件系統(tǒng)在運(yùn)行時(shí)產(chǎn)生的對(duì)象數(shù)量太多,,將導(dǎo)致運(yùn)行代價(jià)過高,帶來系統(tǒng)性能下降等問題,。為了避免系統(tǒng)中出現(xiàn)大量相同或相似的對(duì)象,同時(shí)又不影響客戶端程序通過面向?qū)ο蟮姆绞綄?duì)這些對(duì)象進(jìn)行操作,,享元模式 因此誕生,。
享元模式 (Flyweight Pattern ):通過共享技術(shù)實(shí)現(xiàn)相同或相似對(duì)象的重用,存儲(chǔ)這些對(duì)象的地方稱為享元池(Flyweight Pool ),。
享元模式 以共享的方式高效地支持大量細(xì)粒度對(duì)象的重用,,享元對(duì)象能做到共享的關(guān)鍵是區(qū)分了內(nèi)部狀態(tài) (Intrinsic State )和外部狀態(tài) (Extrinsic State )。
內(nèi)部狀態(tài)是存儲(chǔ)在享元對(duì)象內(nèi)部并且不會(huì)隨著環(huán)境改變而改變的狀態(tài),,內(nèi)部狀態(tài)可以共享,。 外部狀態(tài)是隨環(huán)境改變而改變的、不可以共享的狀態(tài),。通常由客戶端保存,,并在創(chuàng)建享元對(duì)象之后,需要使用時(shí)傳入到享元對(duì)象內(nèi)部,。每個(gè)外部狀態(tài)之間都是相互獨(dú)立的,。 我們可以將具有相同內(nèi)部狀態(tài)的對(duì)象存儲(chǔ)在享元池中,,享元池中的對(duì)象是可以共享的,需要的時(shí)候就將對(duì)象從享元池中取出,,實(shí)現(xiàn)對(duì)象的復(fù)用,。通過向取出對(duì)象注入不同的外部狀態(tài),可以得到一系列相似的對(duì)象,,而這些對(duì)象在內(nèi)存中實(shí)際上只存儲(chǔ)一份,。
享元模式 結(jié)構(gòu)較為復(fù)雜,一般與 簡(jiǎn)單工廠模式 一起使用,,其結(jié)構(gòu)如下圖所示:
圖5 享元模式類圖
Flyweight
(抽象享元類):通常是一個(gè)接口或抽象類,,在抽象享元類中要將內(nèi)部狀態(tài)和外部狀態(tài)分開處理,通常將內(nèi)部狀態(tài)作為享元類的屬性,,而外部狀態(tài)通過注入的方式添加到享元類中,。ConcreteFlyweight
(實(shí)體享元類):它實(shí)現(xiàn)/繼承了抽象享元類,,其實(shí)例稱為享元對(duì)象,。UnsharedConcreteFlyweight
(非共享實(shí)體享元類):并不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設(shè)計(jì)為非共享實(shí)體享元類,,當(dāng)需要一個(gè)非共享實(shí)體享元類的對(duì)象時(shí)可以直接通過實(shí)例化創(chuàng)建,。FlyweightFactory
(享元工廠類):用于創(chuàng)建并管理享元對(duì)象,它針對(duì)抽象享元類編程,,將各種類型的實(shí)體享元對(duì)象存儲(chǔ)在一個(gè)享元池中,,享元池一般設(shè)計(jì)為一個(gè)存儲(chǔ)“鍵值對(duì)”的集合,可以結(jié)合 簡(jiǎn)單工廠模式 進(jìn)行設(shè)計(jì),。當(dāng)用戶請(qǐng)求一個(gè)實(shí)體享元對(duì)象時(shí),,享元工廠提供一個(gè)存儲(chǔ)在享元池中已經(jīng)創(chuàng)建的實(shí)例或者創(chuàng)建一個(gè)新的實(shí)例(如果不存在的話),返回新創(chuàng)建的實(shí)例并將其存儲(chǔ)在享元池中,。在一個(gè)系統(tǒng)中,,通常只有唯一一個(gè)享元工廠,因此可以使用 單例模式 進(jìn)行享元工廠類的設(shè)計(jì),。注意:享元模式 需要維護(hù)一個(gè)記錄了系統(tǒng)已有的所有享元對(duì)象的列表,,而這本身需要耗費(fèi)資源。另外,,為了使享元對(duì)象可以共享,,需要將一些狀態(tài)外部化,使得程序邏輯復(fù)雜化,。因此,,應(yīng)當(dāng)在有足夠多的實(shí)例對(duì)象可供共享時(shí)才值得使用享元模式。
享元模式與字符串
在 C# 語(yǔ)言中,,如果每次執(zhí)行類似string str1 = “abcd”
的操作時(shí)都創(chuàng)建一個(gè)新的字符串對(duì)象將導(dǎo)致內(nèi)存開銷很大,,因此如果第一次創(chuàng)建了內(nèi)容為“abcd”的字符串對(duì)象str1
,,下一次再創(chuàng)建相同的字符串對(duì)象str2
時(shí)會(huì)將它的引用指向str1
,不會(huì)重新分配內(nèi)存空間,,從而實(shí)現(xiàn)了“abcd”在內(nèi)存中的共享,。
見以下程序代碼:
class Program { static void Main (string [] args) { string str1 = "abcd" ; string str2 = "abcd" ; string str3 = "ab" + "cd" ; string str4 = "ab" ; str4 += "cd" ; Console.WriteLine(object.ReferenceEquals(str1, str2)); Console.WriteLine(object.ReferenceEquals(str1, str3)); Console.WriteLine(object.ReferenceEquals(str1, str4)); Console.WriteLine(str1 == str4); Console.WriteLine(str1.Equals(str4)); str2 += "e" ; Console.WriteLine(object.ReferenceEquals(str1, str2)); } }
圖6 程序運(yùn)行結(jié)果
前兩個(gè)輸出語(yǔ)句均為True
,說明str1
,,str2
,,str3
在內(nèi)存中引用了相同的對(duì)象。 字符串str4
的初值為“ab”,,再對(duì)它進(jìn)行操作str4 += “cd”
,,此時(shí)雖然str4
的內(nèi)容與str1
相同,但是由于str4
的初始值不同,,在創(chuàng)建str4
時(shí)重新分配了內(nèi)存,,所以第三個(gè)輸出語(yǔ)句結(jié)果為false
。 最后一個(gè)輸出語(yǔ)句也為false
,,說明當(dāng)對(duì)str2
進(jìn)行修改時(shí)將創(chuàng)建一個(gè)新的對(duì)象,,修改工作在新對(duì)象上完成,而原來引用的對(duì)象并沒有發(fā)生任何改變,,str1
仍然引用原有對(duì)象,,而str2
引用新對(duì)象,str1
與str2
引用了兩個(gè)完全不同的對(duì)象(Copy On Write ),。 注:string
類型是一個(gè)特殊的引用類型,,它的判斷不同于其它引用類型去比較對(duì)象引用是否指向堆中同一實(shí)例,而是和值類型判斷一致,,比較對(duì)象內(nèi)容是否一一相等,。
外觀模式 -- 為外部調(diào)用提供統(tǒng)一入口Sunny 軟件公司欲開發(fā)一個(gè)可應(yīng)用于多個(gè)系統(tǒng)的文件加密模塊,該模塊可以對(duì)文件中的數(shù)據(jù)進(jìn)行加密并將加密之后的數(shù)據(jù)存儲(chǔ)在一個(gè)新文件中,,具體的流程包括三個(gè)部分,,分別是讀取源文件、加密,、保存加密之后的文件,,其中,讀取文件和保存文件使用流來實(shí)現(xiàn),,加密操作通過求模運(yùn)算實(shí)現(xiàn),。
這三個(gè)操作相對(duì)獨(dú)立,為了實(shí)現(xiàn)代碼的獨(dú)立重用,,讓設(shè)計(jì)更符合 單一職責(zé)原則 ,,這三個(gè)操作的業(yè)務(wù)代碼封裝在三個(gè)不同的類中。
通過分析,得到軟件結(jié)構(gòu)如下圖所示:
圖7 文件加密模塊結(jié)構(gòu)圖
實(shí)現(xiàn)代碼如下:
(1)FileReader
:充當(dāng)文件讀取的子系統(tǒng)類,。
internal class FileReader { public string Read (string fileNameSrc) { StringBuilder sb = new StringBuilder(); try { FileStream fs = new FileStream(fileNameSrc, FileMode.Open); int data; while ((data = fs.ReadByte()) != -1 ) { sb.Append((char ) data); } fs.Close(); } catch (Exception e) { Console.WriteLine("讀取文件錯(cuò)誤:" + e.Message); return null; } Console.WriteLine("讀取文件,,獲取明文:" + sb); return sb.ToString(); } }
(2)FileWriter
:充當(dāng)文件保存的子系統(tǒng)類。
internal class FileWriter { public void Write (string encryptStr, string fileNameDes) { try { FileStream fs = new FileStream(fileNameDes, FileMode.Create); byte[] str = Encoding.Default.GetBytes(encryptStr); fs.Write(str, 0 , str.Length); fs.Flush(); fs.Close(); } catch (Exception e) { string info = "保存文件錯(cuò)誤:" + e.Message; Console.WriteLine(info); return ; } Console.WriteLine("保存密文,,寫入文件,。" ); } }
(3)CipherMachine
:充當(dāng)數(shù)據(jù)加密的子系統(tǒng)類。
internal class CipherMachine { public string Encrypt (string plainText) { Console.Write("數(shù)據(jù)加密,,將明文轉(zhuǎn)換為密文:" ); StringBuilder sb = new StringBuilder(); char [] chars = plainText.ToCharArray(); foreach (char ch in chars) { sb.Append((ch%7 )); } Console.WriteLine(sb); return sb.ToString(); } }
(4)EncryptFacade
:充當(dāng)加密外觀類,,為外部調(diào)用提供統(tǒng)一入口。
public class EncryptFacade { private readonly FileReader _reader; private readonly CipherMachine _cipher; private readonly FileWriter _writer; public EncryptFacade () { _reader = new FileReader(); _cipher = new CipherMachine(); _writer = new FileWriter(); } public void FileEncrypt (string fileNameSrc, string fileNameDes) { string plainStr = _reader.Read(fileNameSrc); if (string .IsNullOrEmpty(plainStr)) { return ; } string encryptStr = _cipher.Encrypt(plainStr); _writer.Write(encryptStr, fileNameDes); } }
(5)客戶端代碼:
class Program { static void Main (string [] args) { EncryptFacade ef = new EncryptFacade(); ef.FileEncrypt("src.txt" , "des.txt" ); } }
輸出結(jié)果:
圖8 輸出結(jié)果
在本案例中,,對(duì)文件 src.txt 中的數(shù)據(jù)進(jìn)行加密,,該文件內(nèi)容為 “Hello world!”,加密之后將密文保存到另一個(gè)文件 des.txt 中,,程序運(yùn)行后保存在文件中的密文為“233364062325”,。
在軟件開發(fā)中,有時(shí)候?yàn)榱送瓿梢豁?xiàng)較為復(fù)雜的功能,,一個(gè)客戶類需要和多個(gè)業(yè)務(wù)類交互,,由于涉及到的類比較多,導(dǎo)致使用時(shí)代碼較為復(fù)雜,,此時(shí),,特別需要一個(gè)角色,由它來負(fù)責(zé)和多個(gè)業(yè)務(wù)類進(jìn)行交互,,而客戶類只需與該類交互。
外觀模式 通過引入一個(gè)外觀類(Facade )來充當(dāng)這個(gè)角色,,為多個(gè)業(yè)務(wù)類的調(diào)用提供統(tǒng)一的入口,,簡(jiǎn)化了類與類之間的交互。在外觀模式中,,那些需要交互的業(yè)務(wù)類被稱為子系統(tǒng)(Subsystem ),。
如果沒有外觀類,那么每個(gè)客戶類需要和多個(gè)子系統(tǒng)之間進(jìn)行復(fù)雜的交互,,系統(tǒng)的耦合度將很大,,如圖 9(A)所示;而引入外觀類之后,,客戶類只需要直接與外觀類交互,,客戶類與子系統(tǒng)之間原有的復(fù)雜關(guān)系由外觀類來實(shí)現(xiàn),從而降低了系統(tǒng)的耦合度 ,,如圖 9(B)所示,。
圖9 外觀模式示意圖
外觀模式 并不給系統(tǒng)增加任何新功能,它僅僅是簡(jiǎn)化調(diào)用接口。
外觀模式(Facade Pattern ):又稱為門面模式,,為子系統(tǒng)中的一組接口提供一個(gè)統(tǒng)一的入口,。
外觀模式 是 迪米特法則 的一種具體實(shí)現(xiàn),通過引入一個(gè)新的外觀角色可以降低原有系統(tǒng)的復(fù)雜度,,同時(shí)降低客戶類與子系統(tǒng)的耦合度,。
外觀模式 的結(jié)構(gòu)如下圖所示:
圖10 外觀模式結(jié)構(gòu)圖
Facade
:在 Client 可以調(diào)用它的方法,Facade 知道相關(guān)的(一個(gè)或者多個(gè)) SubSystem 的功能和責(zé)任,;在正常情況下,,它將所有從 Client 發(fā)來的請(qǐng)求委派給相應(yīng)的 SubSystem 處理。
SubSystem
:在軟件系統(tǒng)中可以有一個(gè)或者多個(gè) SubSystem ,,每個(gè) SubSystem 可以不是一個(gè)單獨(dú)的類,,而是一個(gè)類的集合,它實(shí)現(xiàn) SubSystem 的功能,;每一個(gè) SubSystem 都可以被 Client 直接調(diào)用,,或者被 Facade 調(diào)用;SubSystem 并不知道 Facade 的存在,,對(duì)于 SubSystem 而言,,Facade 僅僅是另外一個(gè) Client 而已。
(1)子系統(tǒng)類
internal class SubSystemA { public void MethodA () { //業(yè)務(wù)實(shí)現(xiàn)代碼 } } internal class SubSystemB { public void MethodB () { //業(yè)務(wù)實(shí)現(xiàn)代碼 } } internal class SubSystemC { public void MethodC () { //業(yè)務(wù)實(shí)現(xiàn)代碼 } }
(2)外觀類
public class Facade { private SubSystemA _obj1 = new SubSystemA(); private SubSystemB _obj2 = new SubSystemB(); private SubSystemC _obj3 = new SubSystemC(); public void Method () { _obj1.MethodA(); _obj2.MethodB(); _obj3.MethodC(); } }
(3)客戶端
class Program { static void Main (string [] args) { Facade facade = new Facade(); facade.Method(); } }
抽象外觀類
如果在案例“文件加密模塊”中需要更換一個(gè)加密類,,不再使用原有的基于求模運(yùn)算的加密類CipherMachine
,,而改為基于移位運(yùn)算的新加密類NewCipherMachine
,其代碼如下:
internal string Encrypt (string plainText) { Console.Write("數(shù)據(jù)加密,,將明文轉(zhuǎn)換為密文:" ); StringBuilder sb = new StringBuilder(); int key = 10 ; //設(shè)置密鑰,,移位數(shù)為10 char [] chars = plainText.ToCharArray(); foreach (char ch in chars) { int temp = Convert.ToInt32(ch); //小寫字母移位 if (ch >= 'a' && ch <= 'z' ) { temp += key%26 ; if (temp > 122 ) temp -= 26 ; if (temp < 97 ) temp += 26 ; } //大寫字母移位 if (ch >= 'A' && ch <= 'Z' ) { temp += key%26 ; if (temp > 90 ) temp -= 26 ; if (temp < 65 ) temp += 26 ; } sb.Append((char ) temp)); } Console.WriteLine(sb); return sb.ToString(); }
如果不增加新的外觀類,只能通過修改原有外觀類EncryptFacade
的源代碼來實(shí)現(xiàn)加密類的更換,,將原有的對(duì)CipherMachine
類型對(duì)象的引用改為對(duì)NewCipherMachine
類型對(duì)象的引用,,這違背了 開閉原則 ,因此需要通過增加新的外觀類來實(shí)現(xiàn)對(duì)子系統(tǒng)對(duì)象引用的改變,。
如果增加一個(gè)新的外觀類NewEncryptFacade
來與FileReader
,、FileWriter
以及新增加的NewCipherMachine
類進(jìn)行交互,雖然原有系統(tǒng)類庫(kù)無須做任何修改,,但是因?yàn)榭蛻舳舜a中原來針對(duì)EncryptFacade
類進(jìn)行編程,,現(xiàn)在需要改為NewEncryptFacade
類,因此需要修改客戶端源代碼,。
如何在不修改客戶端代碼的前提下使用新的外觀類呢,?
圖11 引入抽象外觀類之后的文件加密模塊結(jié)構(gòu)圖
(1)抽象外觀類
public abstract class AbstractEncryptFacade { public abstract void FileEncrypt (string fileNameSrc, string fileNameDes) ; }
(2)新增加的實(shí)體外觀類
public class NewEncryptFacade : AbstractEncryptFacade { private readonly FileReader _reader; private readonly NewCipherMachine _cipher; private readonly FileWriter _writer; public NewEncryptFacade () { _reader = new FileReader(); _cipher = new NewCipherMachine(); _writer = new FileWriter(); } public override void FileEncrypt (string fileNameSrc, string fileNameDes) { string plainStr = _reader.Read(fileNameSrc); if (string .IsNullOrEmpty(plainStr)) { return ; } string encryptStr = _cipher.Encrypt(plainStr); _writer.Write(encryptStr, fileNameDes); } }
配置文件
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="facade" value="SunnyFacade.NewEncryptFacade" /> </appSettings> </configuration>
客戶端
using System.Configuration;using System.Reflection;class Program { static void Main (string [] args) { string facadeString = ConfigurationManager. AppSettings["facade" ]; AbstractEncryptFacade ef = Assembly.Load("SunnyFacade" ).CreateInstance(facadeString) as AbstractEncryptFacade; if (ef != null) ef.FileEncrypt("src.txt" , "des.txt" ); } }
輸出結(jié)果如下:
圖12 輸出結(jié)果
原有外觀類EncryptFacade
也需作為抽象外觀類AbstractEncryptFacade
類的子類,更換具體外觀類時(shí)只需修改配置文件,,無須修改源代碼,,符合 開閉原則 ,。
適配器模式 -- 不兼容結(jié)構(gòu)的協(xié)調(diào)Sunny 軟件公司在很久以前曾開發(fā)了一個(gè)算法庫(kù),里面包含了一些常用的算法,,例如排序算法和查找算法,,在進(jìn)行各類軟件開發(fā)時(shí)經(jīng)常需要重用該算法庫(kù)中的算法。
在為某學(xué)校開發(fā)教務(wù)管理系統(tǒng)時(shí),,開發(fā)人員發(fā)現(xiàn)需要對(duì)學(xué)生成績(jī)進(jìn)行排序和查找,,該系統(tǒng)的設(shè)計(jì)人員已經(jīng)開發(fā)了一個(gè)成績(jī)操作接口IScoreOperation
,在該接口中聲明了排序方法Sort(int[])
和查找方法Search(int[], int)
,,為了提高排序和查找的效率,,開發(fā)人員決定重用算法庫(kù)中的快速排序算法類QuickSort
和二分查找算法類BinarySearch
,其中QuickSort
的QuickExchangeSort(int[])
方法實(shí)現(xiàn)了快速排序,,BinarySearch
的BinSearch (int[], int)
方法實(shí)現(xiàn)了二分查找,。
由于某些原因,現(xiàn)在 Sunny 公司開發(fā)人員已經(jīng)找不到該算法庫(kù)的源代碼,,無法直接通過復(fù)制和粘貼操作來重用其中的代碼,;部分開發(fā)人員已經(jīng)針對(duì)IScoreOperation
接口編程,如果再要求對(duì)該接口進(jìn)行修改或要求大家直接使用QuickSort
類和BinarySearch
類將導(dǎo)致大量代碼需要修改,。
Sunny 軟件公司開發(fā)人員面對(duì)這個(gè)沒有源碼的算法庫(kù),,遇到一個(gè)幸福而又煩惱的問題:如何在既不修改現(xiàn)有接口又不需要任何算法庫(kù)代碼的基礎(chǔ)上能夠?qū)崿F(xiàn)算法庫(kù)的重用 ?
圖13 需協(xié)調(diào)的兩個(gè)系統(tǒng)的結(jié)構(gòu)示意圖
現(xiàn)在我們需要IScoreOperation
接口能夠和已有算法庫(kù)一起工作,,讓它們?cè)谕粋€(gè)系統(tǒng)中能夠兼容,,最好的實(shí)現(xiàn)方法是增加一個(gè)適配器角色,通過適配器來協(xié)調(diào)這兩個(gè)原本不兼容的結(jié)構(gòu),。
圖14 算法庫(kù)重用結(jié)構(gòu)圖
IScoreOperation
接口充當(dāng)抽象目標(biāo),,QuickSort
和BinarySearch
類充當(dāng)適配者,OperationAdapter
充當(dāng)適配器,。
(1)抽象成績(jī)操作類:目標(biāo)接口
public interface IScoreOperation { void Sort (int [] array ) ; int Search (int [] array , int key) ; }
(2)快速排序類:適配者
public class QuickSort { public void QuickExchangeSort<T>(T[] array ) where T : IComparable<T> { QuickExchangeSort(array , 0 , array .Length - 1 ); } private void QuickExchangeSort<T>(T[] array , int left, int right) where T : IComparable<T> { if (left < right) { T current = array [left]; int i = left; int j = right; while (i < j) { while (array [j].CompareTo(current) > 0 && i < j) j--; while (array [i].CompareTo(current) <= 0 && i < j) i++; if (i < j) { T temp = array [i]; array [i] = array [j]; array [j] = temp; j--; i++; } } array [left] = array [j]; array [j] = current; if (left < j - 1 ) QuickExchangeSort(array , left, j - 1 ); if (right > j + 1 ) QuickExchangeSort(array , j + 1 , right); } } }
(3)二分查找類:適配者
public class BinarySearch { public int BinSearch<T>(T[] array , T key) where T : IComparable<T> { int left = 0 ; int right = array .Length - 1 ; while (left <= right) { int mid = (left + right)/2 ; if (array [mid].CompareTo(key) < 0 ) left = mid + 1 ; else if (array [mid].CompareTo(key) == 0 ) return mid; else right = mid - 1 ; } return -1 ; } }
(4)操作適配器:適配器
public class OperationAdapter : IScoreOperation { private readonly QuickSort _sortObj; private readonly BinarySearch _searchObj; public OperationAdapter () { _sortObj = new QuickSort(); _searchObj = new BinarySearch(); } public void Sort (int [] array ) { _sortObj.QuickExchangeSort<int >(array ); } public int Search (int [] array , int key) { return _searchObj.BinSearch<int >(array , key); } }
(5)配置文件
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="Adapter" value="SunnyAdapter.OperationAdapter" /> </appSettings> </configuration>
(6)客戶端代碼
using System.Configuration;using System.Reflection;class Program { static void Main (string [] args) { Assembly assembly = Assembly.Load("SunnyAdapter" ); IScoreOperation scoureOperation = assembly.CreateInstance(ConfigurationManager.AppSettings["Adapter" ]) as IScoreOperation; if (scoureOperation == null) return ; int [] scores = { 84 , 76 , 50 , 69 , 90 , 91 , 88 , 96 }; Console.WriteLine("成績(jī)排序結(jié)果:" ); scoureOperation.Sort(scores); for (int i = 0 ; i < scores.Length; i++) { Console.Write(scores[i]+"," ); } Console.WriteLine(); Console.WriteLine("查找成績(jī)90:" ); int score = scoureOperation.Search(scores, 90 ); if (score == -1 ) Console.WriteLine("沒有找到成績(jī)90,。" ); else Console.WriteLine("找到成績(jī)90。" ); Console.WriteLine("查找成績(jī)92:" ); score = scoureOperation.Search(scores, 92 ); if (score == -1 ) Console.WriteLine("沒有找到成績(jī)92,。" ); else Console.WriteLine("找到成績(jī)92。" ); } }
圖15 運(yùn)行結(jié)果
如果需要使用其它排序算法類和查找算法類,,可以增加一個(gè)新的適配器類,,使用新的適配器來適配新的算法,原有代碼無須修改,。
通過引入配置文件和反射機(jī)制,,可以在不修改客戶端代碼的情況下使用新的適配器,無須修改源代碼,,符合“開閉原則 ”,。
適配器模式 (Adpter Pattern )
將一個(gè)接口轉(zhuǎn)換成客戶希望的另一個(gè)接口,使接口不兼容的那些類可以一起工作,其別名為包裝器(Wrapper ),。
在 適配器模式 中引入了一個(gè)被稱為適配器(Adapter )的包裝類,,而它所包裝的對(duì)象稱為適配者(Adaptee ),即被適配的類,。適配器的實(shí)現(xiàn)就是把客戶類的請(qǐng)求轉(zhuǎn)化為對(duì)適配者的相應(yīng)接口的調(diào)用,。從而解決了接口不兼容的問題,使得原本沒有任何關(guān)系的類可以協(xié)同工作,。
根據(jù)適配器類與適配者類的關(guān)系不同,,適配器模式可分為:
對(duì)象適配器(適配器與適配者之間是關(guān)聯(lián)關(guān)系) 類適配器(適配器與適配者之間是繼承或?qū)崿F(xiàn)關(guān)系)
圖16 對(duì)象適配器模式結(jié)構(gòu)圖
(1)目標(biāo)接口
public interface ITarget { void Request () ; }
(2)適配者類
public class Adaptee { public void SpecificRequest () { ; } }
(3)適配器類
public class Adapter : ITarget { private readonly Adaptee _adaptee; public Adapter (Adaptee adaptee) { _adaptee = adaptee; } public void Request () { _adaptee.SpecificRequest(); } }
在對(duì)象適配器中,客戶端需要調(diào)用Request()
方法,,而適配者類Adaptee
沒有該方法,,但是它所提供的SpecificRequest()
方法卻是客戶端所需要的。為了使客戶端能夠使用適配者類,,需要提供一個(gè)包裝類Adapter
,,即適配器類。這個(gè)包裝類包裝了一個(gè)適配者的實(shí)例,,從而將客戶端與適配者銜接起來,,在適配器的Request()
方法中調(diào)用適配者的SpecificRequest()
方法。
圖17 類適配器模式結(jié)構(gòu)圖
public class Adapter : Adaptee, ITarget { public void Request () { base.SpecificRequest(); } }
適配器模式 包含以下 3 個(gè)角色:
ITarget
(目標(biāo)接口):定義客戶所需接口,。Adaptee
(適配者類):即被適配的角色,,它定義了一個(gè)已經(jīng)存在的接口,這個(gè)接口需要適配,,適配者類一般是一個(gè)具體類,,包含了客戶希望使用的業(yè)務(wù)方法,在某些情況下可能沒有適配者類的源碼,。Adapter
(適配器類):適配器可以調(diào)用另一個(gè)接口,,作為一個(gè)轉(zhuǎn)換器,對(duì)Adaptee
和ITarget
進(jìn)行適配,,它通過實(shí)現(xiàn)ITarget
并 關(guān)聯(lián)/繼承 一個(gè)Adaptee 對(duì)象/類 使二者產(chǎn)生聯(lián)系,。由于Java、C#等語(yǔ)言不支持多重類繼承,,因此類適配器的使用受到很多限制,,例如如果目標(biāo)ITarget不是接口,,而是一個(gè)類,,就無法使用類適配器,;此外,如果適配者Adaptee
為sealed
類,,也無法使用類適配器,。在C#等面向?qū)ο缶幊陶Z(yǔ)言中,,大部分情況下我們使用的是對(duì)象適配器,類適配器較少使用,。
橋接模式 -- 處理多維度變化Sunny 軟件公司欲開發(fā)一個(gè)跨平臺(tái)圖像瀏覽系統(tǒng),,要求該系統(tǒng)能夠顯示 BMP 、JPG ,、GIF ,、PNG 等多種格式的文件,并且能夠在 Windows ,、Linux ,、Unix 等多個(gè)操作系統(tǒng)上運(yùn)行。
系統(tǒng)首先將各種格式的文件解析為像素矩陣(Matrix
),,然后將像素矩陣顯示在屏幕上,,在不同的操作系統(tǒng)中可以調(diào)用不同的繪制函數(shù)來繪制像素矩陣。
系統(tǒng)需具有較好的擴(kuò)展性以支持新的文件格式和操作系統(tǒng),。
Sunny 軟件公司的開發(fā)人員針對(duì)上述要求,,提出了一個(gè)初始設(shè)計(jì)方案,其基本結(jié)構(gòu)如下圖所示:
圖18 跨平臺(tái)圖像瀏覽器初始結(jié)構(gòu)圖
在上圖的初始設(shè)計(jì)方案中,,使用了一種多層繼承結(jié)構(gòu) ,。
Image
是抽象父類,而每一種類型的圖像類作為其直接子類,,不同的圖像文件格式具有不同的解析方法,,可以得到不同的像素矩陣;由于每一種圖像又需要在不同的操作系統(tǒng)中顯示,,不同的操作系統(tǒng)在屏幕上顯示像素矩陣有所差異,,因此需要為不同的圖像類再提供一組在不同操作系統(tǒng)顯示的子類。 對(duì)該設(shè)計(jì)方案進(jìn)行分析,,發(fā)現(xiàn)存在如下兩個(gè)主要問題:
【1】由于采用了多層繼承結(jié)構(gòu) ,,導(dǎo)致系統(tǒng)中類的個(gè)數(shù)急劇增加,在各種圖像的操作系統(tǒng)實(shí)現(xiàn)層提供了12個(gè)實(shí)體類,,加上各級(jí)抽象層的類,,系統(tǒng)中類的總個(gè)數(shù)達(dá)到了17個(gè),在該設(shè)計(jì)方案中,,實(shí)體層的類的個(gè)數(shù) = 所支持的圖像文件格式數(shù) × 所支持的操作系統(tǒng)數(shù)
,。
【2】系統(tǒng)擴(kuò)展麻煩,由于每一個(gè)實(shí)體類既包含圖像文件格式信息,,又包含操作系統(tǒng)信息,因此無論是增加新的圖像文件格式還是增加新的操作系統(tǒng),,都需要增加大量的實(shí)體類,。
如何解決這兩個(gè)問題 ,?
談?wù)剝煞N常見文具 “毛筆 ” 和 “蠟筆 ” 的區(qū)別。
假如我們需要大中小 3 種型號(hào)的畫筆,,能夠繪制 12 種不同的顏色 ,。如果使用蠟筆,需要準(zhǔn)備 36支,,如果使用毛筆,,只需要提供 3 種型號(hào)的毛筆,外加 12 個(gè)顏料盒即可,,涉及到的對(duì)象個(gè)數(shù)為 15,,遠(yuǎn)小于36,卻能實(shí)現(xiàn)與36支蠟筆同樣的功能,。如果增加一種新型號(hào)的畫筆,,并且也需要具有 12 種顏色 ,對(duì)應(yīng)的蠟筆需增加 12支,,而毛筆只需增加 1支,。
為什么會(huì)這樣呢?
通過分析可知:
蠟筆:顏色和型號(hào)兩個(gè)不同的變化維度(即兩個(gè)不同的變化原因)融合在一起,,無論是對(duì)顏色還是對(duì)型號(hào)進(jìn)行擴(kuò)展都勢(shì)必會(huì)影響另一個(gè)維度,; 毛筆:顏色和型號(hào)實(shí)現(xiàn)了分離,增加新的顏色或者型號(hào)對(duì)另一方都沒有任何影響,。 如果使用軟件工程中的術(shù)語(yǔ),,我們可以認(rèn)為在蠟筆中顏色和型號(hào)之間存在較強(qiáng)的耦合性,而毛筆很好地將二者解耦,,使用起來非常靈活,,擴(kuò)展也更為方便。
我們通過分析得知,,該系統(tǒng)也存在兩個(gè)獨(dú)立變化的維度:
圖像文件格式(對(duì)應(yīng)圖像格式的解析) 操作系統(tǒng)(對(duì)應(yīng)像素矩陣的顯示)
圖19 跨平臺(tái)圖像瀏覽器中存在的兩個(gè)獨(dú)立變化維度示意圖
為了減少所需生成的子類數(shù)目,,將這兩個(gè)維度分離,使得它們可以獨(dú)立變化,,增加新的圖像文件格式或者操作系統(tǒng)時(shí)都對(duì)另一個(gè)維度不造成任何影響,。
Sunny 公司開發(fā)人員重構(gòu)了系統(tǒng)的設(shè)計(jì),如下圖所示:
圖20 跨平臺(tái)圖像瀏覽器重構(gòu)后的結(jié)構(gòu)圖
完整代碼如下:
//各種格式的文件最終都被轉(zhuǎn)化為像素矩陣,, //不同的操作系統(tǒng)提供不同的方式顯示像素矩陣,。 public class Matrix { //... }
“實(shí)現(xiàn)類”層次結(jié)構(gòu):
public abstract class ImageImp { public abstract void DoPaint (Matrix m) ; }public class LinuxImp : ImageImp { public override void DoPaint (Matrix m) { Console.WriteLine("在Linux操作系統(tǒng)中顯示圖像。" ); } }public class UnixImp : ImageImp { public override void DoPaint (Matrix m) { Console.WriteLine("在Unix操作系統(tǒng)中顯示圖像,。" ); } }public class WindowsImp : ImageImp { public override void DoPaint (Matrix m) { Console.WriteLine("在Windows操作系統(tǒng)中顯示圖像,。" ); } }
“抽象類”層次結(jié)構(gòu):
public abstract class Image { protected ImageImp Imp; public void SetImageImp (ImageImp imp) { this .Imp = imp; } public abstract void ParseFile (string fileName) ; }public class BmpImage : Image { public override void ParseFile (string fileName) { Matrix m = new Matrix(); Imp.DoPaint(m); Console.WriteLine(fileName + ",格式為BMP,。" ); } }public class GifImage : Image { public override void ParseFile (string fileName) { Matrix m = new Matrix(); Imp.DoPaint(m); Console.WriteLine(fileName + ",,格式為GIF,。" ); } }public class JpgImage : Image { public override void ParseFile (string fileName) { Matrix m = new Matrix(); Imp.DoPaint(m); Console.WriteLine(fileName+ ",格式為JPG,。" ); } }public class PngImage : Image { public override void ParseFile (string fileName) { Matrix m = new Matrix(); Imp.DoPaint(m); Console.WriteLine(fileName + ",,格式為PNG。" ); } }
配置文件代碼:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="image" value="SunnyBridge.BmpImage" /> <add key="os" value="SunnyBridge.LinuxImp" /> </appSettings> </configuration>
客戶端代碼:
using System.Reflection;using System.Configuration;class Program { static void Main (string [] args) { string image = ConfigurationManager.AppSettings["image" ]; string os = ConfigurationManager.AppSettings["os" ]; Assembly assembly = Assembly.Load("SunnyBridge" ); Image img = assembly.CreateInstance(image) as Image; ImageImp imageImp = assembly.CreateInstance(os) as ImageImp; if (img != null) { img.SetImageImp(imageImp); img.ParseFile("光頭強(qiáng)" ); } } }
輸出結(jié)果如下圖所示:
圖21 運(yùn)行結(jié)果
如果需要更換圖像文件格式或者更換操作系統(tǒng),,只需修改配置文件即可,,在實(shí)際使用時(shí),可以通過分析圖像文件格式后綴名來確定具體的文件格式,,在程序運(yùn)行時(shí)獲取操作系統(tǒng)信息來確定操作系統(tǒng)類型,,無須使用配置文件 。
當(dāng)增加新的圖像文件格式或者操作系統(tǒng)時(shí),,原有系統(tǒng)無需做任何修改,,只需增加一個(gè)對(duì)應(yīng)的實(shí)現(xiàn)類即可,系統(tǒng)具有較好的可擴(kuò)展性,,完全符合“開閉原則 ”,。
橋接模式與多層繼承方案不同,它將兩個(gè)獨(dú)立變化的維度設(shè)計(jì)為兩個(gè)獨(dú)立的繼承等級(jí)結(jié)構(gòu),,并且在抽象層建立一個(gè)抽象關(guān)聯(lián),,該關(guān)聯(lián)關(guān)系類似一條連接兩個(gè)獨(dú)立繼承結(jié)構(gòu)的橋,故名橋接模式,。
橋接模式 (Bridge Pattern ):將抽象部分 與它的實(shí)現(xiàn)部分 分離,,使它們都可以獨(dú)立地變化。
用 抽象關(guān)聯(lián) 取代了傳統(tǒng)的 多繼承 ,; 將類之間的 靜態(tài)繼承關(guān)系 轉(zhuǎn)換為 動(dòng)態(tài)的對(duì)象組合關(guān)系 ,; 橋接模式 結(jié)構(gòu)如下圖所示:
圖22 橋接模式類圖
Abstraction
(抽象類):定義了一個(gè)Implementor
類型的對(duì)象并可以維護(hù)該對(duì)象,它與Implementor
之間具有關(guān)聯(lián)關(guān)系,,它既可以包含抽象業(yè)務(wù)方法,,也可以包含具體業(yè)務(wù)方法。RefinedAbstraction
(擴(kuò)充抽象類):實(shí)現(xiàn)了在Abstraction
中聲明的抽象業(yè)務(wù)方法,,在RefinedAbstraction
中可以調(diào)用在Implementor
中定義的業(yè)務(wù)方法,。Implementor
(實(shí)現(xiàn)類接口):通過關(guān)聯(lián)關(guān)系,在Abstraction
中不僅擁有自己的方法,,還可以調(diào)用到Implementor
中定義的方法,,使用關(guān)聯(lián)關(guān)系來替代繼承關(guān)系。ConcreteImplementor
(實(shí)體實(shí)現(xiàn)類):在程序運(yùn)行時(shí),,ConcreteImplementor
對(duì)象將替換其父類對(duì)象,,提供給抽象類實(shí)體的業(yè)務(wù)操作方法。通常情況下,我們將具有兩個(gè)獨(dú)立變化維度的類的一些普通業(yè)務(wù)方法和與之關(guān)系最密切的維度設(shè)計(jì)為“抽象類”層次結(jié)構(gòu)(抽象部分) ,,而將另一個(gè)維度設(shè)計(jì)為“實(shí)現(xiàn)類”層次結(jié)構(gòu)(實(shí)現(xiàn)部分) ,。
對(duì)于毛筆而言:
型號(hào)是其固有的維度,因此可以設(shè)計(jì)一個(gè)抽象的毛筆類,,在該類中聲明并部分實(shí)現(xiàn)毛筆的業(yè)務(wù)方法,而將各種型號(hào)的毛筆作為其子類,; 顏色是其另一個(gè)維度,,由于它與毛筆之間存在一種“設(shè)置”的關(guān)系,因此我們可以提供一個(gè)抽象的顏色接口,,而將具體的顏色作為實(shí)現(xiàn)該接口的子類,。 結(jié)構(gòu)示意圖如下圖所示:
圖23 橋接模式類圖
橋接模式 中體現(xiàn)了“單一職責(zé)原則 ”、“開閉原則 ”,、“合成復(fù)用原則 ”,、“里氏代換原則 ”、“依賴倒轉(zhuǎn)原則 ”等設(shè)計(jì)原則,。熟悉該模式有助于我們深入理解這些設(shè)計(jì)原則,,也有助于我們形成正確的設(shè)計(jì)思想和培養(yǎng)良好的設(shè)計(jì)風(fēng)格。
適配器模式與橋接模式的聯(lián)用
橋接模式 :用于系統(tǒng)的初步設(shè)計(jì),,對(duì)于存在兩個(gè)獨(dú)立變化維度的類可以將其分為抽象化 和實(shí)現(xiàn)化 兩個(gè)角色,,使它們可以分別進(jìn)行變化;適配器模式 :初步設(shè)計(jì)完成之后,,當(dāng)發(fā)現(xiàn)系統(tǒng)與已有類無法協(xié)同工作時(shí),,可以采用適配器模式 ,解決兩個(gè)已有接口間不兼容問題,;某系統(tǒng)的報(bào)表處理模塊中,,需要將報(bào)表顯示和數(shù)據(jù)采集分開,系統(tǒng)可以有多種報(bào)表顯示方式也可以有多種數(shù)據(jù)采集方式,,如可以從文本文件中讀取數(shù)據(jù),,也可以從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù),還可以從Excel 文件中獲取數(shù)據(jù),。如果需要從Excel 文件中獲取數(shù)據(jù),,則需要調(diào)用與Excel 相關(guān)的API ,而這個(gè)API 是現(xiàn)有系統(tǒng)所不具備的,,該API 由廠商提供,。
在設(shè)計(jì)過程中,由于存在報(bào)表顯示和數(shù)據(jù)采集兩個(gè)獨(dú)立變化的維度,,因此可以使用 橋接模式 進(jìn)行初步設(shè)計(jì),;為了使用Excel 相關(guān)的API 來進(jìn)行數(shù)據(jù)采集則需要使用適配器模式。系統(tǒng)的完整設(shè)計(jì)中需要將兩個(gè)模式聯(lián)用,如下圖所示:
圖24 橋接模式與適配器模式聯(lián)用示意圖