一
问题的引出
前一段时间,我在写一个跟踪和管理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的代码如下:
using System;
namespace BugBusiness
{
/**//// <summary>
/// A data provider,and it is an abstract class,so there is a class have to inhrits from it!
/// </summary>
public abstract class DataProvider
{
/**//// <summary>
/// To check if user is valid
/// </summary>
/// <param name="name">user name</param>
/// <param name="password">user password</param>
/// <returns>
/// True if user is valid,or else return false
///</returns>
public abstract int CheckUser(User user);
}
public class User
{
private string name;
private string password;
/**//// <summary>
/// Property Name
/// </summary>
public string Name
{
get
{
return name;
}
set
{
name=value;
}
}
/**//// <summary>
/// Property Password
/// </summary>
public string Password
{
get
{
return password;
}
set
{
password=value;
}
}
}
public class Login
{
public static bool IsValidUser(User user)
{
BugData.SqlDataProvider dp=new BugData.SqlDataProvider();
if(dp.CheckUser(user)==1)
return true;
return false;
}
}
}
SqlDataProvider的代码如下:
using System;
using BugBusiness;
namespace BugData
{
/**//// <summary>
/// Summary description for Class1.
/// </summary>
public class SqlDataProvider:BugBusiness.DataProvider
{// <summary>
/**//// This method is to check the user name/password is valid
/// </summary>
/// <param name="user">
/// the user to check
/// </param>
/// <returns>
/// 1 ,user name/password is valid
/// 2 ,not valid
/// </returns>
/// <remarks>This method is to simulate accessing database</remarks>
public override int CheckUser(BugBusiness.User user)
{
if(user.Name=="Name" && user.Password=="123")
return 1;
return 2;
}
}
}
咋一看起来这个没有错.是呀,在语法上面没有错,但是在编译的时候除了问题.为什么?
二
问题的解释
上面的代码编译会出现问题,为什么?经过仔细的琢磨,才明白其中的缘由.
让我们先看看其中的类图,不知道发现了什么.也许你很容易看出问题,
但是在实际解决的时候可能就不会注意这个问题了.
本类图是一个很糟糕的设计.分析如下:
我们可以从类图和代码中发现,设计致力于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编译,会报出版本调用不一致的错误.即使不报错误,在以后的维护中够我们受的了.本来分层就是为了使项目简单,易于维护,到现在却事与愿违.
三
问题的解决
方案1:反射
既然原因已经知道,那么该如何解决呢?有人肯定会问,在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(),参考下面代码:
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Reflection;
public static DataProvider Instance(string connectionString,string databaseOwner,string providerTypeName)
{
Type type = null;
type = Type.GetType( providerTypeName );
// Insert the type into the cache
//
Type[] paramTypes = new Type[2];
paramTypes[0] = typeof(string);
paramTypes[1] = typeof(string);
object[] paramArray = new object[2];
paramArray[0] = databaseOwner;
paramArray[1] = connectionString;
return (DataProvider)( ((ConstructorInfo)type.GetConstructor(paramTypes)). Invoke(paramArray) );
}
在SqlDataProvider里增加:
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.
有了实例化之后,应该如何调用,请看如下代码:
public class Login
{
public static bool IsValidUser(User user)
{
BugBusiness.DataProvider dp =BugBusiness.DataProvider.Instance("Server=localhost;uid=sa;pwd=;database=northwind","dbo",
"BugData.SqlDataProvider,BugData" );
return dp.CheckUser(user);
}
}
这样即可.
最后,类图可以为:
方案2:固定程序集的版本
这个方案就我个人而言我不太赞成,但是也是最简单的办法,就是控制版本,比如一直是0.9.0.如何控制,很简单,在每个工程里都有一个AssemblyInfo.cs,里面有一行
[assembly:
AssemblyVersion("1.0.*")]
的代码.这个就是版本,你只需要写入固定的版本,那么无论怎么编译,它的版本都不会改变
四
应用
关于反射的应用非常多.比如微软提供的PetShop和Duwamish就用到类似的反射性质.许多著名的开源项目如AspNetForums也用到了.
五
感谢
在此,感谢我的3位同事,Ming
Wang, Nancy Huang,Nanco Xing.
附录: 源代码
1) 没有使用反射的源代码
下载
2) 使用反射的源代码
下载 |