封裝變化(二)

來源:互聯網
上載者:User
 

 

考慮一個日誌記錄工具。目前需要提供一個方便的日誌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”即可。這種結構具有很好的可擴充性,較好地解決了未來可能發生的需求變化所帶來的問題。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.