圖解“管道過濾器模式”應用例項:SOD框架的命令執行管道
管道和過濾器
管道和過濾器是八種體系結構模式之一,這八種體系結構模式是:層、管道和過濾器、黑板、代理者、模型-檢視-控制器(MVC) 表示-抽象-控制(PAC)、微核、映像。
管道和過濾器適用於需要漸增式處理資料流的領域,而常見的“層”模式它 能夠被分解成子任務組,其中每個子任務組處於一個特定的抽象層次上。
按照《POSA(面向模式的軟體架構)》裡的說法,管道過濾器(Pipe-And-Filter)應該屬於架構模式,因為它通常決定了一個系統的基本架構。管道過濾器和生產流水線類似,在生產流水線上,原材料在流水線上經一道一道的工序,最後形成某種有用的產品。在管道過濾器中,資料經過一個一個的過濾器,最後得到需要的資料。
管道&過濾器模型的基本部件都有一套輸入輸出介面。每個部件從輸入介面中讀取資料,經過處理,將結果資料置於輸出介面中,這樣的部件稱為“過濾器”。這種模型的連線者將一個過濾器的輸出傳送到另一個過濾器的輸入, 我們把這種連線者稱為“管道”。在這種模型中,過濾器必須是獨立的實體,每一個過濾器的狀態不受其它過濾器的影響,並且,雖然人們對過濾器的輸入輸出有一定的規定,但過濾器並不需要知道向它提供資料流的過濾器和 它要提供資料流的過濾器的內部細節。任何兩個過濾器,只要它們之間傳送的資料遵守共同的規約就可以相連線。 每個過濾器都有自己獨立的輸入輸出介面,如果過濾器間傳輸的資料遵守其規約,只要用管道將它們連線就可以正常工作。
查詢的關注點
基於以上管道和過濾器特點,它為處理資料流的系統提供了一種良好的結構,每一個處理步驟封裝在一個過濾器元件中,資料通過相鄰的過濾器之間的管道傳輸。在程式處理中,也有類似的這種資料流,最常見的就是命令處理的資料流,它從最開始的查詢命令,到最後的結果輸出,會經過多個步驟,以ADO.NET來說,執行一個查詢會經過以下過程:
查詢命令:
- 獲取資料集:
- 開啟資料庫連線 IDbConnection
- 建立命令物件 IDbCommand
- 建立資料介面卡 IDataAdapter
- 填充資料集 IDataAdapter.Fill(DataSet)
- 關閉資料庫連線
- 返回資料集 DataSet
- 獲取資料閱讀器
- 開啟資料庫連線 IDbConnection
- 建立命令物件 IDbCommand
- 執行資料閱讀器查詢 IDbCommand.ExecuteReader
- 返回資料閱讀器 IDataReader
- 關閉資料庫連線
非查詢命令:
- 開啟資料庫連線 IDbConnection
- 建立命令物件 IDbCommand
- 執行查詢 IDbCommand.ExecuteNonQuery()
- 關閉資料庫連線
可以看到,上面這幾種查詢命令的執行,都要經過幾個相同的步驟:開啟資料庫連線,建立命令物件,執行查詢,返回結果,關閉資料庫連線,這幾個步驟是有嚴格順序的,前後依賴的,就像水流一般,因此,我們也可以利用“管道--過濾器”模式,在查詢命令的執行過程中,插入某些特定的處理邏輯。
從最終使用者的角度來說,一個查詢有4個關注點:
- 查詢前
- 查詢中
- 查詢後
- 查詢異常
其中,查詢中是ADO.NET等資料訪問元件內部的處理過程,一般不能直接提供使用者可以切入和干預的觀察點,那麼剩下3個關注點,就是我們可以用的,這3個關注點,就像一個水管的三個閥門一樣。
SOD框架的命令處理管道
命令處理介面
SOD框架現在也提供了這樣的三個關注點,使得使用元件的使用者,能夠無需修改元件內部的程式碼,改變和觀察元件的處理情況,這三個關注點對應的是 ICommandHandle介面的3個方法:
/// <summary>
/// 查詢命令處理器介面
/// </summary>
public interface ICommandHandle
{
/// <summary>
/// 獲取當前適用的資料庫型別,如果通用,請設定為 UNKNOWN
/// </summary>
DBMSType ApplayDBMSType { get; }
/// <summary>
/// 執行前處理,比如預處理SQL,補充設定引數型別,返回是否繼續進行查詢執行
/// </summary>
/// <param name="db">資料庫訪問物件</param>
/// <param name="SQL"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns>返回真,以便最終執行查詢,否則將終止查詢</returns>
bool OnExecuting(CommonDB db, ref string SQL, CommandType commandType, IDataParameter[] parameters);
/// <summary>
/// 執行過程中出錯情況處理
/// </summary>
/// <param name="cmd"></param>
/// <param name="errorMessage"></param>
void OnExecuteError(IDbCommand cmd, string errorMessage);
/// <summary>
/// 查詢執行完成後的處理,不管是否執行出錯都會進行的處理
/// </summary>
/// <param name="cmd"></param>
/// <param name="recordAffected">命令執行的受影響記錄行數</param>
long OnExecuted(IDbCommand cmd, int recordAffected);
}
一圖勝千言,先看下面的“SOD框架命令處理管道”圖:
由前面介面的定義並結合這個圖,可以看到查詢命令在“資料訪問”這個管道里面流動過程:
- 首先,它在 OnExecuting 這個過濾插口位置改變命令的行為特徵,比如SQL預處理,終止查詢等,發起非同步操作等;
- 接著,查詢命令由Ado.Net進行處理,而此時是很有可能發生查詢錯誤的情況的,那麼提供一個OnExecuteError 過濾插口,讓錯誤資訊可以被一些過濾器使用,比如查詢操作日誌元件;
- 最後,不論前面命令執行是否成功,命令執行完了還需要進行一些其它的處理,那麼提供一個OnExecuteError 過濾插口,比如觀察命令執行的結果行/影響行,命令的執行時間,返回非同步通知等。
根據這裡定義的命令執行管道介面,最典型的實現就是可以用來記錄查詢日誌,比如下面的 CommandExecuteLogHandle 類:
/// <summary>
/// 命令執行日誌處理器,可以記錄SQL和引數,執行時間等資訊
/// </summary>
public class CommandExecuteLogHandle :ICommandHandle
{
/// <summary>
/// 初始化一個命令執行日誌處理器
/// </summary>
public CommandExecuteLogHandle()
{
this.CurrCommandLog = new CommandLog(true);
//這裡需要進行一些初始化檢查,設定日誌路徑等
if (CommandLog.DataLogFile == null)
CommandLog.DataLogFile = "~/sql.log";
CommandLog.SaveCommandLog = true;
}
public CommandLog CurrCommandLog { get; private set; }
public bool OnExecuting(CommonDB db, ref string SQL, CommandType commandType, IDataParameter[] parameters)
{
this.CurrCommandLog.ReSet();
return true;
}
public void OnExecuteError(IDbCommand cmd, string errorMessage)
{
CurrCommandLog.WriteErrLog(cmd, "AdoHelper:" + errorMessage);
}
public long OnExecuted(IDbCommand cmd, int recordAffected)
{
long elapsedMilliseconds;
CurrCommandLog.WriteLog(cmd, "AdoHelper", out elapsedMilliseconds);
CurrCommandLog.WriteLog("RecordAffected:"+recordAffected , "AdoHelper");
return elapsedMilliseconds;
}
public DBMSType ApplayDBMSType
{
get { return DBMSType.UNKNOWN; }
}
}
注意,這裡 ApplayDBMSType 返回 UNKNOW,表示當前介面實現類性適合於任意資料庫查詢的情況。
另外,日誌過濾器內部使用了框架內建的 CommandLog 類,它可以非同步的記錄SQL執行情況,並能記錄查詢時間大於某個值的查詢,詳細請看《PDF.NET的SQL日誌》。
再看下面,我們實現一個用於處理Oracle查詢的“過濾器”元件,它會在查詢開始前,對SQL進行一些預處理,比如將本來使用於SQLSERVER的SQL語句格式,處理成Oracle特有的格式:
/// <summary>
/// 自定義的Oracle命令處理器,用於處理特殊的欄位名大寫問題
/// </summary>
public class OracleCommandHandle : ICommandHandle
{
public bool OnExecuting(CommonDB db, ref string sql, System.Data.CommandType commandType, System.Data.IDataParameter[] parameters)
{
sql= sql.Replace("[", "").Replace("]", "").Replace("@", ":").ToUpper();
//設定SQLSERVER相容性為假,避免命令物件真正執行的時候再進行Oracle的查詢語句的預處理。
db.SqlServerCompatible = false;
//返回真,以便最終執行查詢,否則將終止查詢
return true;
}
public void OnExecuteError(System.Data.IDbCommand cmd, string errorMessage)
{
}
public long OnExecuted(System.Data.IDbCommand cmd, int recordAffected)
{
return 1;
}
public PWMIS.Common.DBMSType ApplayDBMSType
{
get { return PWMIS.Common.DBMSType.Oracle; }
}
}
注意:上面這個實現類,指明瞭當前命令執行過濾器元件,僅使用於Oracle資料庫,當前如果是其它資料庫型別,會忽略該過濾器元件。
除此之外,是不是還可以寫一個過濾器元件,監視下當前查詢是否執行成功,如果成功,將查詢的SQL和引數傳送到訊息佇列,進行非同步更新其它資料庫?
開閉原則
所以,SOD框架的“命令執行管道”給予了終端使用者在不改變原有資料訪問元件的內部實現的情況下,一個監視和處理命令執行過程的“視窗”,一個或者多個對查詢命令的“過濾器”元件,這正是面向物件原則之一的開閉原則。
我們來看下百度百科對開閉原則的解釋:
開閉原則(OCP)是面向物件設計中“可複用設計”的基石,是面向物件設計中最重要的原則之一,其它很多的設計原則都是實現開閉原則的一種手段。
遵循開閉原則設計出的模組具有兩個主要特徵:
(1)對於擴充套件是開放的(Open for extension)。這意味著模組的行為是可以擴充套件的。當應用的需求改變時,我們可以對模組進行擴充套件,使其具有滿足那些改變的新行為。也就是說,我們可以改變模組的功能。
(2)對於修改是關閉的(Closed for modification)。對模組行為進行擴充套件時,不必改動模組的原始碼或者二進位制程式碼。模組的二進位制可執行版本,無論是可連結的庫、DLL或者.EXE檔案,都無需改動。
既然命令執行管道如此有用,我們該如何使用呢?還是直接看示例程式碼比較簡單:
/// <summary>
/// 用來測試的本地 資料庫上下文類
/// </summary>
public class MyOracleDbContext : DbContext
{
public MyOracleDbContext()
: base("local")
{
//local 是連線字串名字
//註冊日誌處理器和Oracle命令處理器
base.CurrentDataBase.RegisterCommandHandle(new CommandExecuteLogHandle());
base.CurrentDataBase.RegisterCommandHandle(new OracleCommandHandle());
}
#region 父類抽象方法的實現
protected override bool CheckAllTableExists()
{
//建立使用者表
CheckTableExists<User>();
return true;
}
#endregion
}
在這個 MyOracleDbContext 類中,我們註冊了2個過濾器元件:日誌過濾器和Oracle命令過濾器。
如果當前連線配置名 local 對應的資料庫訪問提供程式不是Oracle了怎麼辦?
不用擔心,前面說過, Oracle命令過濾器僅對Oracle資料訪問有效,其它資料庫訪問會忽略,而日誌過濾器元件它是適用於任何資料庫訪問的。
上面的示例程式碼中,CurrentDataBase 物件其實就是 SOD框架的 AdoHelper物件,所以,只要你使用SOD框架,那麼不管你使用的是框架的ORM,SQL-MAP,Data Controls功能,甚至是最簡單的“SqlHelper”類應用,你都可以享受到SOD框架的“命令執行管道”帶給你d便利!
與“觀察者模式”的區別
.NET框架中,對觀察者模式最常見的實現就是“事件”,事件可以實現監視某個物件的改變情況然後發起事件通知,最後由事件處理程式完成處理。在本文描述的查詢處理場景中,也可以在查詢處理前,處理後,發生異常這3個“觀察點”發起事件,並且,事件也可以實現“多播”,一個事件可以由多個事件處理程式來處理。所以,從這個意義上來說,“管道-過濾器”模式跟“觀察者”模式功能上很相似的,但為何SOD框架不選擇後者來實現呢?
我認為,主要區別有以下幾個方面:
在架構層面上,
“管道-過濾器”模式通常用於架構設計層面,是一種“架構模式”,比如分層架構;而觀察者模式一種面向物件程式設計的模式,運用的領域不一樣。
“管道-過濾器”模式讓架構實現鬆耦合;而觀察者模式的觀察者和被觀察者之間,往往是緊密耦合的關係。
在具體使用形式上,
“架構模式”可以通過配置檔案來提供附件的一種功能實現,比如ASP.NET的HttpHandle,ASP.NET MVC的Controller上的Filter等,所以它的實現是鬆耦合的;
而觀察者模式往往體現在編寫的程式碼中,用事件來處理程式碼來實現,所以它往往是緊耦合的。
在業務語義上,
“管道-過濾器”是用於處理流動的載體的,比如資料,資訊或其它具有流動特性的物體,方便進行多環節,多層次的攔截或者加工處理,並且每個處理環節都有序的,流動和有序,這是這類業務最重要的特徵;
“事件”處理的客體範圍更廣,事件的客體沒有固定的形態,事件的發生和處理可能都是無序的。
其它方面的考慮,事件使用前總是需要宣告事件掛鉤,會多增加一些程式碼量,並且使用完成之後,往往還需要解除掛鉤,否則可能發生記憶體洩漏,請參見 我另外一篇文章《Release編譯模式下,事件是否會引起記憶體洩漏問題初步研究》。
總結
所以,在當前這個資料查詢的場景中,對於查詢命令的處理,採用“管道-過濾器”模式來實現一個命令執行管道,是最合適的,它讓人在業務語義上更加明確,並且使用上更加靈活,程式碼實現量也最小,而且不需要修改原有的程式碼實現,符合開閉原則。
到目前為止,我還沒有看到其它 資料處理框架/ORM框架 比較明確的提供了關注和干預元件內部查詢執行過程的功能,都只能進行外部的攔截,如果你有這樣的需求,來試試SOD框架帶給你的靈活和自由吧!
附註:
SOD不僅僅是一個ORM,它還有SQL-MAP和DataControl,具體可以看框架官網 http://www.pwmis.com/sqlmap ,9年曆史鑄就的成果,堅固可靠。
非常感謝你看到這裡,相信你初步瞭解了SOD框架的基本功能,如果您還有其它問題,歡迎你在專案的開源網站 http://pwmis.codeplex.com的討論區發帖,或者去官方部落格相關文章回帖也可。