設計模式精髓—封裝變化
模式,是為了需求變動而產生,拋開需求談模式,顯得很蒼白。 無論是創建型模式、結構型模式還是行為型模式,歸根結底都是尋找軟件中可能存在的“變化”,然后利用抽象的方式對這些變化進行封裝。由於抽象沒有具體的實現,就代表了一種無限的可能性,使得其擴展成為了可能。
創建型模式的目的就是封裝對象創建的變化。例如Factory Method模式和Abstract Factory模式,建立了專門的抽象的工廠類,以此來封裝未來對象的創建所引起的可能變化。而Builder模式則是對對象內部的創建進行封裝,由於細節對抽象的可替換性,使得將來面對對象內部創建方式的變化,可以靈活的進行擴展或替換。
至於結構型模式,它關注的是對象之間組合的方式。本質上說,如果對象結構可能存在變化,主要在於其依賴關系的改變。當然對於結構型模式來說,處理變化的方式不僅僅是封裝與抽象那么簡單,還要合理地利用繼承與聚合的方法,靈活地表達對象之間的依賴關系。例如Decorator模式,描述的就是對象間可能存在的多種組合方式,這種組合方式是一種裝飾者與被裝飾者之間的關系,因此封裝這種組合方式,抽象出專門的裝飾對象顯然正是“封裝變化”的體現。同樣地,Bridge模式封裝的則是對象實現的依賴關系,而Composite模式所要解決的則是對象間存在的遞歸關系。
行為型模式關注的是對象的行為。行為型模式需要做的是對變化的行為進行抽象,通過封裝以達到整個架構的可擴展性。例如策略模式,就是將可能存在變化的策略或算法抽象為一個獨立的接口或抽象類,以實現策略擴展的目的。或者封裝一個請求(Command模式),或者封裝一種狀態(State模式),或者封裝“訪問”的方式(Visitor模式),或者封裝“遍歷”算法(Iterator模式)。而這些所要封裝的行為,恰恰是軟件架構中最不穩定的部分,其擴展的可能性也最大。將這些行為封裝起來,利用抽象的特性,就提供了擴展的可能。
利用設計模式,通過封裝變化的方法,可以最大限度的保證軟件的可擴展性。面對紛繁復雜的需求變化,雖然不可能完全解決因為變化帶來的可怕夢魘,然而,如能在設計之初預見某些變化,仍有可能在一定程度上避免未來存在的變化為軟件架構帶來的災難性傷害。
比如拿工廠模式來說:應用場所是------創建對象的方法相同,但創建具體的對象會經常變化。 換句話說:我的代碼中許多地方都要產生一個新的對象,而這個創新的對象在未來一段時間內,有可能會經常變化。當這個對象真的要變了時,我如何用最少的代價修 改我的代碼呢?因此,有必要把"創新對象"進行解耦。盡可能的,我只改一兩處,就實現 了所有代碼的修改。如何實現呢? 凡是要生成新對象的代碼,都統一用個類的方法產 實例,如果對象要變時,修改這個類就行了。
抽象工廠模式的定義:抽象工廠模式提供了一個接口,用於創建相關或依賴對象的家族,而不需要明確指定具體的類。抽象工廠允許客戶使用抽象的接口來創建一組相關的產品,而不需要知道實際的具體產品是什么,這樣客戶就從具體的產品中被解耦了。
舉個例子http://blog.csdn.net/rainylin/article/details/1715308 : 我們需要設計一個數據庫組件,它能夠訪問微軟的Sql Server數據庫。根據ADO.Net的知識,我們需要使用如下的對象:SqlConnection, SqlCommand, SqlDataAdapter等。
如果僅就Sql Server而言,在訪問數據庫時,我們可以直接創建這些對象:
SqlConnection connection = new SqlConnection(strConnection);//連接數據庫
SqlCommand command = new SqlCommand(connection);//創建數據庫操作語句
SqlDataAdapter adapter = new SqlDataAdapter();//對結果集進行處理
在一個數據庫組件中,充斥着如上的語句,顯然是不合理的。它充滿了僵化的壞味道,一旦要求支持其他數據庫時,原有的設計就需要徹底的修改,這為擴展帶來了困難。
那么我們來思考一下,以上的設計應該做怎樣的修改?假定該數據庫組件要求或者將來要求支持多種數據庫,那么對於Connection,Command,DataAdapter等對象而言,就不能具體化為Sql Server的對象。也就是說,我們需要為這些對象建立一個繼承的層次結構,為他們分別建立抽象的父類,或者接口。然后針對不同的數據庫,定義不同的具體類,這些具體類又都繼承或實現各自的父類,例如Connection對象:
Connection對象的層次結構
我為Connection對象抽象了一個統一的IConnection接口,而支持各種數據庫的Connection對象都實現了IConnection接口。同樣的,Command對象和DataAdapter對象也采用了相似的結構。現在,我們要創建對象的時候,可以利用多態的原理創建:
IConnection connection = new SqlConnection(strConnection);
從這個結構可以看到,根據訪問的數據庫的不同,對象的創建可能會發生變化。也就是說,我們需要設計的數據庫組件,以現在的結構來看,仍然存在無法應對對象創建發生變化的問題。利用“封裝變化”的原理,我們有必要把創建對象的責任單獨抽象出來,以進行有效地封裝。例如,如上的創建對象的代碼,就應該由專門的對象來負責。我們仍然可以建立一個專門的抽象工廠類DBFactory,並由它負責創建Connection,Command,DataAdapter對象。至於實現該抽象類的具體類,則與目標對象的結構相同,根據數據庫類型的不同,定義不同的工廠類,類圖如圖四所示:
DBFactory的類圖
是一個典型的Abstract Factory模式的體現。類DBFactory中的各個方法均為abstract方法,所以我們也可以用接口來代替該類的定義。繼承DBFactory類的各個具體類,則創建相對應的數據庫類型的對象。以SqlDBFactory類為例,創建各自對象的代碼如下:
public class SqlDBFactory: DBFactory
{
public override IConnection CreateConnection(string strConnection)
{
return new SqlConnection(strConnection);
}
public override ICommand CreateCommand(IConnection connection)
{
return new SqlCommand(connection);
}
public override IDataAdapter CreateDataAdapter()
{
return new SqlDataAdapter();
}
}
現在要創建訪問Sql Server數據庫的相關對象,就可以利用工廠類來獲得。首先,我們可以在程序的初始化部分創建工廠對象:
DBFactory factory = new SqlDBFactory();
然后利用該工廠對象創建相應的Connection,Command等對象:
IConnection connection = factory.CreateConnection(strConnection);
ICommand command = factory.CreateCommand(connection);
由於我們利用了封裝變化的原理,建立了專門的工廠類,以封裝對象創建的變化。可以看到,當我們引入工廠類后,Connection,Command等對象的創建語句中,已經成功地消除了其與具體的數據庫類型相依賴的關系。在如上的代碼中,並未出現Sql之類的具體類型,如SqlConnection、SqlCommand等。也就是說,現在創建對象的方式是完全抽象的,是與具體實現無關的。無論是訪問何種數據庫,都與這幾行代碼無關。至於涉及到的數據庫類型的變化,則全部抽象到DBFactory抽象類中了。需要更改訪問數據庫的類型,我們也只需要修改創建工廠對象的那一行代碼,例如將Sql Server類型修改為Oracle類型:DBFactory factory = new OracleDBFactory();
很顯然,這樣的方式提高了數據庫組件的可擴展性。我們將可能發生變化的部分封裝起來,放到程序固定的部分,例如初始化部分,或者作為全局變量,更可以將這些可能發生變化的地方,放到配置文件中,通過讀取配置文件的值,創建相對應的對象。如此一來,不需要修改代碼,也不需要重新編譯,僅僅是修改xml文件,就能實現數據庫類型的改變。例如,我們創建如下的配置文件:
創建工廠對象的代碼則相應修改如下:
string factoryName = ConfigurationSettings.AppSettings[“db”].ToString();
//DBLib為數據庫組件的程序集:
DBFactory factory = (DBFactory)Activator.CreateInstance(“DBLib”,factoryName).Unwrap();
為數據庫組件的程序集:當我們需要將訪問的數據庫類型修改為Oracle數據庫時,只需要將配置文件中的Value值修改為“OracleDBFactory”即可。這種結構具有很好的可擴展性,較好地解決了未來可能發生的需求變化所帶來的問題。
再來個策略模式的例子
設想這樣一個需求,我們需要為自己的框架提供一個負責排序的組件。目前需要實現的是冒泡排序算法和快速排序算法,根據“面向接口編程”的思想,我們可以為這些排序算法提供一個統一的接口ISort,在這個接口中有一個方法Sort(),它能接受一個object數組參數。對數組進行排序后,返回該數組。接口的定義如下:
public interface ISort
{
void Sort(ref object[] beSorted);
}
其類圖如下:
然而一般對於排序而言,排列是有順序之分的,例如升序,或者降序,返回的結果也不相同。最簡單的方法我們可以利用if語句來實現這一目的,例如在QuickSort類中:
public class QuickSort:ISort
{
private string m_SortType;
public QuickSort(string sortType)
{
m_SortType = sortType;
}
public void Sort(ref object[] beSorted)
{
if (m_SortType.ToUpper().Trim() == “ASCENDING”)
{
//執行升序的快速排序;
}
else
{
//執行降序的快速排序;
}
}
}
當然,我們也可以將string類型的SortType定義為枚舉類型,減少出現錯誤的可能性。然而仔細閱讀代碼,我們可以發現這樣的代碼是非常僵化的,一旦需要擴展,如果要求我們增加新的排序順序,例如字典順序,那么我們面臨的工作會非常繁重。也就是說,變化產生了。通過分析,我們發現所謂排序的順序,恰恰是排序算法中最關鍵的一環,它決定了誰排列在前,誰排列在后。然而它並不屬於排序算法,而是一種比較的策略,后者說是比較的行為。
如果仔細分析實現ISort接口的類,例如QuickSort類,它在實現排序算法的時候,需要對兩個對象作比較。按照重構的做法,實質上我們可以在Sort方法中抽取出一個私有方法Compare(),通過返回的布爾值,決定哪個對象在前,哪個對象在后。顯然,可能發生變化的是這個比較行為,利用“封裝抽象”的原理,就應該為該行為建立一個專有的接口ICompare,然而分別定義實現升序、降序或者字典排序的類對象。
我們在每一個實現了ISort接口的類構造函數中,引入ICompare接口對象,從而建立起排序算法與比較算法的弱耦合關系(因為這個關系與抽象的ICompare接口相關),例如QuickSort類:
public class QuickSort:ISort
{
private ICompare m_Compare;
public QuickSort(ICompare compare)
{
m_Compare= compare;
}
public void Sort(ref object[] beSorted)
{
//實現略
for (int i = 0; i < beSorted.Length - 1; i++)
{
if (m_Compare.Compare(beSorted[i],beSorted[i+1))
{
//略;
}
}
//實現略
}
}
最后的類圖如下:
通過對比較策略的封裝,以應對它的變化,顯然是Stategy模式的設計。事實上,這里的排序算法也可能是變化的,例如實現二叉樹排序。由於我們已經引入了“面向接口編程”的思想,我們完全可以輕易的添加一個新的類BinaryTreeSort,來實現ISort接口。對於調用方而言,ISort接口的實現,同樣是一個Strategy模式。此時的類結構,完全是一個對擴展開發的狀態,它完全能夠適應類庫調用者新需求的變化。
