文/玄魂
背景
最近一直在和同事讨论单元测试的问题,在对已有代码的可测试性进行评估的时候,我们发现业务逻辑层和持久层的测试分离成为了难点。
正常而言,对业务逻辑的单元测试是要同持久层分离开的。为了确保业务逻辑层的可测试性,要求业务逻辑层依赖持久层的接口而不是实现,这样在进行单元测试的时候,可以灵活的使用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,目前为止我已经给出了一个极其简单但是五脏俱全的例子了,当然这可能不是最好的解决方案。希望对您能有所帮助。