考慮一個日志記錄工具。目前需要提供一個方便的日志API,使得客戶可以輕松地完成日志的記錄。該日志要求被記錄到指定的文本文件中,記錄的內容屬于字符串類型,其值由客戶提供。我們可以非常容易地定義一個日志對象:
public class Log
{
public void Write(string target, string log)
{
//實現內容;
}
}
當客戶需要調用日志的功能時,可以創建日志對象,完成日志的記錄:
Log log = new Log();
log.Write(“error.log”, “log”);
然而隨著日志記錄的頻繁使用,有關日志的文件越來越多,日志的查詢與管理也變得越不方便。此時,客戶提出,需要改變日志的記錄方式,將日志內容寫入到指定的數據表中。顯然,如果仍然按照前面的設計,具有較大的局限性。
現在我們回到設計之初,想象一下對于日志API的設計,需要考慮到這樣的變化嗎?這里存在兩種設計理念,即漸進的設計和計劃的設計。從本例來分析,要求設計者在設計初就考慮到日志記錄方式在未來的可能變化,并不容易。再者,如果在最開始就考慮全面的設計,會產生設計上的冗余。因此,采用計劃的設計固然具有一定的前瞻性,但一方面對設計者的要求過高,同時也會產生一些缺陷。那么,采用漸進的設計時,遇到需求變化時,利用重構的方法,改進現有的設計,又需要考慮未來的再一次變化嗎?這是一個見仁見智的問題。對于本例而言,我們完全可以直接修改Write()方法,接受一個類型判斷的參數,從而解決此問題。但這樣的設計,自然要擔負因為未來可能的再一次變化,而導致代碼大量修改的危險,例如,我們要求日志記錄到指定的Xml文件中。
所以,變化是完全可能的。在時間和技術能力允許的情況下,我更傾向于將變化對設計帶來的影響降低到最低。此時,我們需要封裝變化。
在封裝變化之前,我們需要弄清楚究竟是什么發生了變化?從需求看,是日志記錄的方式發生了變化。從這個概念分析,可能會導致兩種不同的結果。一種情形是,我們將日志記錄的方式視為一種行為,確切的說,是用戶的一種請求。另一種情形則從對象的角度來分析,我們將各種方式的日志看作不同的對象,它們調用接口相同的行為,區別僅在于創建的是不同的對象。前者需要我們封裝“用戶請求的變化”,而后者需要我們封裝“日志對象創建的變化”。
封裝“用戶請求的變化”,在這里就是封裝日志記錄可能的變化。也就是說,我們需要把日志記錄行為抽象為一個單獨的接口,然后才分別定義不同的實現。如圖一所示:

圖一:封裝日志記錄行為的變化
如果熟悉設計模式,可以看到圖一所表示的結構正是Command模式的體現。由于我們對日志記錄行為進行了接口抽象,用戶就可以自由地擴展日志記錄的方式,只需要實現ILog接口即可。至于Log對象,則存在與ILog接口的弱依賴關系:
public class Log
{
private ILog log;
public Log(ILog log)
{
this.log = log;
}
public void Write(string target, string logValue)
{
log.Execute(target, logValue);
}
}
我們也可以通過封裝“日志對象創建的變化”實現日志API的可擴展性。在這種情況下,日志會根據記錄方式的不同,被定義為不同的對象。當我們需要記錄日志時,就創建相應的日志對象,然后調用該對象的Write()方法,實現日志的記錄。此時,可能會發生變化的是需要創建的日志對象,那么要封裝這種變化,就可以定義一個抽象的工廠類,專門負責日志對象的創建,如圖二所示:

圖二:封裝日志對象創建的變化
圖二是Factory Method模式的體現,由抽象類LogFactory專門負責Log對象的創建。如果用戶需要記錄相應的日志,例如要求日志記錄到數據庫,需要先創建具體的LogFactory對象:
LogFactory factory = new DBLogFactory();
當在應用程序中,需要記錄日志,那么再通過LogFactory對象來獲取新的Log對象:
Log log = factory.Create();
log.Write(“ErrorLog”, “log”);
如果用戶需要改變日志記錄的方式為文本文件時,僅需要修改LogFactory對象的創建即可:
LogFactory factory = new TxtFileLogFactory();
為了更好地理解“封裝對象創建的變化”,我們再來看一個例子。假如,我們需要設計一個數據庫組件,它能夠訪問微軟的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文件,就能實現數據庫類型的改變。例如,我們創建如下的配置文件:
<appSettings>
<add key=”db” value=”SqlDBFactory”/>
</appSettings>
創建工廠對象的代碼則相應修改如下:
string factoryName = ConfigurationSettings.AppSettings[“db”].ToString();
//DBLib為數據庫組件的程序集:
DBFactory factory = (DBFactory)Activator.CreateInstance(“DBLib”,factoryName).Unwrap();
當我們需要將訪問的數據庫類型修改為Oracle數據庫時,只需要將配置文件中的Value值修改為“OracleDBFactory”即可。這種結構具有很好的可擴展性,較好地解決了未來可能發生的需求變化所帶來的問題。