文/玄魂
背景
最近一直在和同事討論單元測試的問題,在對已有代碼的可測試性進行評估的時候,我們發現業務邏輯層和持久層的測試分離成為了難點。
正常而言,對業務邏輯的單元測試是要同持久層分離開的。為了確保業務邏輯層的可測試性,要求業務邏輯層依賴持久層的接口而不是實現,這樣在進行單元測試的時候,可以靈活的使用Mock和數據庫來填充數據。
但是我們的代碼規范規定,Dao層的方法必須是靜態方法,而且之前的業務邏輯代碼在邏輯內部調用Dao,二者緊緊的耦合在一起。現在面臨的問題是Dao層的方法必須是靜態方法,我們沒有辦法提取接口。初步討論,為了達到可測試性,有以下幾個改造方案:
l 將業務邏輯層調用的Dao類提取出公有變量,然后調用方實施屬性注入或者構造函數注入的方式。實現了業務邏輯的可測試性,但是沒有實現業務邏輯和持久層的解耦。
l 新建接口封裝對Dao的調用。實現了可測試性,也實現了業務邏輯和持久層的解耦,但是違反了Dao方法必須靜態的初衷,如果不考慮單元測試,接口包裝的方式在設計上顯得臃腫,而且改造起來代碼量大。
l 使用dynamic類型聲明Dao類,在業務邏輯的構造工廠中依賴注入實現。這種方法對代碼的改動量最小,但是失去了編譯時檢查的優勢;同時每種調用類型都是事先知道的,不是dynamic類型的標准應用;這里使用dynamic類型只是利用它的運行時綁定的特性,實現類似接口的功能,不得已而為之。
總之,如果不實現真正的解耦,任何方案都是勉強。本篇文章討論上述最后一種方案的實施過程中遇到的一個dynamic 類型變量調用靜態方法的解決方案,同時兼顧單元測試,和分層解耦。這種方案也不是我的原創,參考鏈接:http://blogs.msdn.com/b/davidebb/archive/2009/10/23/using-c-dynamic-to-call-static-members.aspx
dynamic 類型調用靜態方法
我先模擬一個Dao的實現,類名為ReportItemDao,只有一個方法,名為GetItemDescriptionAndCode,如下:
public class ReportItemDao
{
/// <summary>
/// 根據條目ID獲取條目描述列表
/// </summary>
/// <param name="itemID">條目ID</param>
/// <returns>當前條目的描述列表</returns>
/// <remarks>玄魂-2012-1-11創建</remarks>
public static Dictionary<string, string> GetItemDescriptionAndCode(Guid itemID)
{
Dictionary<string, string> itemDesList = new Dictionary<string, string>();
Database database = Database.GetDatabase(StaticParameters.CONNECTIONSTRINGS_NAME_Design);
SafeProcedure.ExecuteAndGetInstanceList(database, @"[dbo].[GetReportTempDesByItemID]",
parameters =>
{
parameters.AddWithValue(@"itemID", itemID);
},
(IRecord record, int entity) =>
{
itemDesList.Add(record.Get<string>(@"Code"), record.Get<string>(@"ReportItemDecription"));
}
);
return itemDesList;
}
}
再模擬一個業務邏輯層的代碼,調用上面的方法,如下:
public class ReportItemProvider : IReportItemProvider
{
public Dictionary<string, string> GetItemDescriptionAndCode(Guid itemID)
{
return ReportItemDao.GetItemDescriptionAndCode(itemID);
}
}
上面的調用代碼也很簡單,沒有任何邏輯,實際場景下會復雜得多。首先,在ReportItemProvider類聲明一個dynamic字段,名為ReportItemDao,取消對ReportItemDao類的命名空間的引入。修改之后的代碼如下:
public class ReportItemProvider : IReportItemProvider
{
dynamic ReportItemDaoDynamic;
public Dictionary<string, string> GetItemDescriptionAndCode(Guid itemID)
{
return ReportItemDaoDynamic.GetItemDescriptionAndCode(itemID);
}
}
上面的代碼是理想狀態,並不能運行成功,因為我們無法將ReportItemDao類賦值給dynamic類型,實例化的類型是無法調用靜態方法的。
ReportItemDao類的實例不能賦值給ReportItemDaoDynamic,那我們只能傳一個ReportItemDao的Type類實例給ReportItemDaoDynamic,別無他法。傳遞一個Type類實例和dynamic類型,意味着在執行具體方法時必須執行反射,但是dynamic類型目前還不支持這樣的調用,我們必須對它的調用過程進行重寫。
下面的代碼實現了對dynamic類型的自定義。先創建一個名為StaticMembersDynamicWrapper的類,繼承自DynamicObject類,然后重寫它的TryGetMember和TryInvokeMember方法,利用反射找到靜態方法並執行。
public class StaticMembersDynamicWrapper : DynamicObject {
private Type _type;
public StaticMembersDynamicWrapper(Type type) { _type = type; }
// Handle static properties
public override bool TryGetMember(GetMemberBinder binder, outobjectresult) {
PropertyInfo prop = _type.GetProperty(binder.Name, BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public);
if(prop == null) {
result = null;
returnfalse;
}
result = prop.GetValue(null, null);
returntrue;
}
// Handle static methods
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, outobjectresult) {
MethodInfo method = _type.GetMethod(binder.Name, BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public);
if(method == null) {
result = null;
returnfalse;
}
result = method.Invoke(null, args);
returntrue;
}
}
dynamic 類型調用靜態方法的問題解決了,還需要一個對象工廠對dynamic 類型的變量進行依賴注入。
在依賴注入之前,我還要一個簡單的Ioc容器來存儲Dao類的Type,代碼如下:
public static class DaoContainer
{
private static Dictionary<string, Type> daoDic = new Dictionary<string, Type >();
static DaoContainer ()
{
daoDic.Add(“ReportItemDaoDynamic”,typeOf(ReportItemDao));
}
public static Type GetTypeInstance(string key)
{
return daoDic[key];
}
}
下面我們用一個簡單的工廠類來創建ReportItemProvider。
class ProviderFactory
{
public T GetInstance<T>() where T : class
{
var instance = ReportItemProvider.Instance;//單例
SetDao(instance);
return instance;
}
}
private void SetDao(object obj)
{
Type type = obj.GetType();
FieldInfo[]fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
foreach (FieldInfo field in fields)
{
if (field.GetValue(obj)==null)
{
//這里判斷是否應該賦值,省略……
string name = field.Name;
field.SetValue(obj, DaoContainer. GetTypeInstance(name));
}
}
}
ok,目前為止我已經給出了一個極其簡單但是五臟俱全的例子了,當然這可能不是最好的解決方案。希望對您能有所幫助。