前一段时间,我在写一个跟踪和管理Bug的程序,编程语言为C#.
本软件采用经典的多层架构.我将软件分成UI Layer, Bussiness Layer, 和Data Access Layer.
问题就是出现这里.我最开始的写法是,在Business Layer定义了几个类,假设为DataProvider,User,Login,在Data Access Layer定义了SqlDataProvider.
他们的关系如下:
DataProvider为一个纯虚函数.
SqlDataProvder继承DataProvider..
User为用户信息类.
Login为登陆信息类,其中有一方法CheckUser()验证用户的有效性.
并且我对3层各自建立了一个工程,也就是说这个Solution包括3个工程,假设BugUI,BugBusiness,BugData.当Build之后生成3个DLL文件,分别是UI.dll,BugBusiness.dll,BugData.dll.
其中DataProvider的代码如下:
SqlDataProvider的代码如下:
咋一看起来这个没有错.是呀,在语法上面没有错,但是在编译的时候除了问题.为什么?
上面的代码编译会出现问题,为什么?经过仔细的琢磨,才明白其中的缘由.
让我们先看看其中的类图,不知道发现了什么.也许你很容易看出问题, 但是在实际解决的时候可能就不会注意这个问题了.
本类图是一个很糟糕的设计.分析如下:
我们可以从类图和代码中发现,设计致力于Business层和Data层.在这里我们只罗列出一个很简单的问题, 即验证用户的有效型. Login和User都在Business层, 只有SqlDataProvider在Data层.其实你很快就会发现Business层调用了Data层, Data层又调用了Business层.调用图如下.
从上图可以看出,调用是双向的.现在你可以看出其中的问题的吧.如果你还没有看出,在心里面可能已经有了一个印象,隐隐约约感到其中的不合理之处.
说了这么多,那么到底会产生什么不良的影响呢?
这种设计比较晦涩,导致结构层次不清,往往难以维护,有时甚至是出错.这样说可能是有点抽象,那就具体一点说吧,以前面的Case为例,举出其中的影响.
假设Business层的单独的Project编译的程序集DLL是BugBusiness.DLL,Data层的工程编译的程序集是BugData.DLL.同时假设BugBusiness.DLL的版本是0.9.0,BugData.DLL的版本也是0.9.0.Ok,这里是起点.我接下来再编译一次,BugBusiness.dll版本变为0.9.1,BugData的版本也变为0.9.1.
这里就出现了一个问题.BugBusiness.dll是基于0.9.0的BugData.dll,但是现在确实0.9.1.同理,BugData.dll本应基于0.9.0的BugBusiness.dll,现在却是0.9.1.我们可以用下面的图表示:
说明:实线表示应该调用的
虚线表示实际调用的
如果在.NET编译,会报出版本调用不一致的错误.即使不报错误,在以后的维护中够我们受的了.本来分层就是为了使项目简单,易于维护,到现在却事与愿违.
既然原因已经知道,那么该如何解决呢?有人肯定会问,在Business层的DataProvider好像没有多大作用,我之所以设计这个类,就是考虑到了工层的可扩展性,我现在用的是Microsoft SQL Server,如果哪天我用Oracle,My Sql,甚至其他,只需要继承DataProvider即可,例如OracleDataProvider,这样你只需要在写配置文件的时候说明用到的数据库是Oracle.
我想解决的方法就是避免双向调用,那么是去掉Business层调用Data层呢,还是去掉Data层调用Business层呢?显然Data层调用Business层是不可避免的,那么只有去掉Business层调用Data层,但是你可能就会问,我怎么去掉呢,Login肯定会用到SqlDataProvider呀?问题就是在这里了.
我不知道你发现DataProvider这个类没有,要知道DataProvider是一个abstract类呀.根据面向对象的性质,调用抽象类时,其实是调用其实现它的子类,即调用SqlDataProvider.现在应该明白了吧.
你很有可能晦写出如下代码:
DataProvider dp=new DataProvider();
很遗憾,编译器肯定会告诉你,你不可以实现一个抽象类的实例.怎么样,是不是有些晕.但是既然这样,我们该如何实现呢?答案是反射.我们可以利用反射来创建一个实例.
如何创建,只需在DataProvider增加一个静态的实例方法Instance(),参考下面代码:
public SqlDataProvider(string connectionString,string databaseOwner)
{
…
}
请注意第4行
using System.Reflection;
它引用了反射命名空间.如何进行反射,下面解释一下,
type = Type.GetType( providerTypeName );
它得到构造的实例的类型,在这里可以是SqlDataProvider.因为在SqlDataProvider构造时有两个参数,并且是string类型,所以paramTypes都为string类型,如果是int型,请用typeof(int);定义了类型之后,最后传入参数的值,即databaseOwner;和connectionString.这些准备完之后,现在正式开始构造,利用ConstructorInfo这个类,使用前面定义的参数类型数组和参数值数组即可创建.更多的详情参考MSDN.
有了实例化之后,应该如何调用,请看如下代码:
这样即可.
这个方案就我个人而言我不太赞成,但是也是最简单的办法,就是控制版本,比如一直是0.9.0.如何控制,很简单,在每个工程里都有一个AssemblyInfo.cs,里面有一行
[assembly: AssemblyVersion("1.0.*")]
的代码.这个就是版本,你只需要写入固定的版本,那么无论怎么编译,它的版本都不会改变
关于反射的应用非常多.比如微软提供的PetShop和Duwamish就用到类似的反射性质.许多著名的开源项目如AspNetForums也用到了.
在此,感谢我的3位同事,Ming Wang, Nancy Huang,Nanco Xing.
附录: 源代码
1) 没有使用反射的源代码 下载
2) 使用反射的源代码 下载