DotNet(.Net)下构建高适应性的三层架构
我想所有的企业级系统开发的.Net程序员都和我一样,曾经苦苦挣扎于底层架构的搭建,如何一次性简单快速的搭建出足够前台调用的接口便成为一个欲罢不能的想法。参与了若干系统的开发之后,经过头脑风暴,我尝试着构建了一种简单的架构实现过程,在经过一些尝试后最终构建成功,并实施在我的一个解决方案当中了,现将构建方案与大家分享如下。
写了很久的程序,养成一个习惯,就是坚持用尽量少的代码实现尽量多的事情,所以一般能共用的代码,我就写成共用的,这样基本上就减少了不少的代码量。另外,通过一定的抽象过程,本人已经总结出规律,并成功的开发出一个底层代码自动化工具,基本上我尝试了一下,一个有二十六个表的系统,二十分钟内,我就完成了底层(包括存储过程类)的构建,这个过程如果纯手工的话,我觉得效率高也要一两周吧。
(注:下文所有例举的代码都由自动化工具生成)
我的架构中有PetShop的影子,也有Duwamish的痕迹。
Solution中包含六个Project:
1、 BussinessRules:规则层,主要存放一些公用的规则类,比如对图片的处理类,对Excel的处理类。
2、 Common:数据实体类,与数据库表一一对应,在这一点上,有些人可能说一一对应可能并不怎么行,在我看来,数据实体类的目的就是为了抽象数据表结构的,那么完全可以直接没有逻辑的把数据表抽象出来,应该少考虑点逻辑上的事情。至于你要想同时想对不同实体里的东西进行混合操作,也完全没有必要独立定义出一个新的数据实体的。
3、 DataAccess:数据访问层,与数据实体一一对应,基本上一个数据访问类对应一个数据实体,通过对数据实体属性的控制来实现数据的添加、更新、删除和检索。
4、 Web:这一层当然就是PetShop中的表示层,Duwamish的Web层,主要是与用户交互的。许多逻辑上的事情得在这一层的CodeBehind里来写。
5、 WinTest:这是个WinForm,用来在构建底层结构的时候,对相关方法进行测试的,一般做项目的时候,应该有这样一个测试工程,养成写好一个方法测试一个方法的习惯,这样到后面写Web层的时候,就不用再返回头来改底层了。
6、 SystemFramework:系统参数的初始化及日志的处理工程。
本文的重点放在Common和DataAccess这两个工程上,当然也会包括存储过程部分。我们拿一个表作例子来讲:
1、Table结构:
Table:Product_Info | |||||
PK:ProductID | |||||
Index:none | |||||
Field Name |
Type |
Length |
Null |
Default |
Description |
ProductID |
Int |
4 |
N |
(‘’) |
产品ID |
SerialNumber |
Nvarchar |
50 |
N |
(‘’) |
产品序列号 |
Price |
Decimal |
4 |
N |
0 |
价格 |
Author |
Nvarchar |
50 |
|
|
作者 |
AuthorWebSite |
Nvarchar |
100 |
|
|
作者网站 |
TypeID |
Int |
4 |
N |
0 |
产品类型ID |
OperationSystem |
Int |
4 |
N |
0 |
支持的平台系统 |
ReleaseDate |
Datetime |
8 |
N |
GetDate |
发布时间 |
HitCount |
Int |
4 |
N |
0 |
点击数 |
Description |
Ntext |
16 |
|
|
产品描述 |
AddTime |
Datetime |
8 |
|
|
添加时间 |
这个表比较有代表性,字段类型基本上含概了我们经常使用的几种数据类型:Int、Nvarchar、Decimal、DateTime、Ntext,我比较喜欢带N的数据类型,这样是有利于多语言方面的扩展,语言切换不会有问题。另外对数据表有一个特殊的要求,就是不管怎么样都要加上一个自增长的栏位,否则会产生N多代码,而且添加一个自增长的栏位不影响系统,我只把数据库当成存储数据的地方,不加任何逻辑,逻辑由程序员在开发过程中用程序控制。
2、数据实体类
数据实体类命名与数据表名一致(Product_Info.cs)包含两部分,私有成员和公共属性(两者一一对应)。但会跟数据类型不同而有不同的对应关系:
数字型和时间型的一个字段对应三个公共属性,分别为字段本身、字段的极小值和字段的极大值,后两者主要为了检索设置的。如果是不是上面的两种类型,都归为字符型,字符型除了自身外,会多一个用来模糊搜索的属性。除了上面的所有,还会有一个属性ReturnCount,控制返回数据的条数。
另外私有成员都必须赋初值,规律是数字型的都赋-1,字符型的赋null,日期型的为
举例:
ProductID栏位会对应三个属性:
/// Public Attribute:ProductID
/// </summary>
public int ProductID
{
get{return m_ProductID;}
set{m_ProductID=value;}
}
/// <summary>
/// Public Attribute:ProductID_Min,for scale
/// </summary>
public int ProductID_Min
{
get{return m_ProductID_Min;}
set{m_ProductID_Min=value;}
}
/// <summary>
/// Public Attribute:ProductID_Min,for scale
/// </summary>
public int ProductID_Max
{
get{return m_ProductID_Max;}
set{m_ProductID_Max=value;}
}
大家一目了然,势必已经看出端倪了,通过这三个属性的范围控制和ReturnCount的信息量的控制,我们完全可以检索出各种针对ProductID的检索。这只是一个栏位的控制,那么如果再加另外一些栏位控制,我们完全可以对各种条件下的产生的数据进行数据操作。
3、数据访问类:
数据访问类名字是数据实体名加个“s”,针对上面的类,数据访问类就是“Product_Infos.cs”。数据实体类包含六个部分:
1、 构造函数:构造函数有两个,一个是系统默认的构建函数,另外一个构造函数有一个参数,就是对应表的主键ID,所以在实例话类的时候,可以不用给主键ID属性赋值,就可以获取一条信息。当然,如果喜欢统一用属性控制也无可厚非。因为比较简单,代码就不列了。
2、 构建参数:这是个私有方法,用来构建传给存储过程的参数,输出参数主键ID的入参和出参是必须的,其后其它的属性就是有选择性的往里面传了,而且这里判断要不要传进去主要是根据在实例化数据实体的初始值判断,如果等于初始值,就表示外面没有传参数进来,那么就相应的不传给存储过程。当然这里要解释一种情况,就是假如你就是想取等于默认值的信息怎么办,我想,这种情况就需要自己在构建实体类的时候尽量把默认值设为不可能存在的值就行了。此私有方法被操作数据库的四类方法(添加、修改、删除、检索)共同调用。
{
ArrayList sqlParameterList=new ArrayList();
sqlParameterList.Add(MakeOutParam("@OutID",SqlDbType.Int,4));
sqlParameterList.Add(MakeInParam("@ProductID", SqlDbType.Int, 4, product_Info.ProductID));
if(product_Info.ProductID_Min!=-1)
sqlParameterList.Add(MakeInParam("@ProductID_Min", SqlDbType.Int, 4, product_Info.ProductID_Min));
if(product_Info.ProductID_Max!=-1)
sqlParameterList.Add(MakeInParam("@ProductID_Max", SqlDbType.Int, 4, product_Info.ProductID_Max));
//略去N行
if(product_Info.ReturnCount!=-1)
sqlParameterList.Add(MakeInParam("@ReturnCount", SqlDbType.Int, 4, product_Info.ReturnCount));
SqlParameter[] prams= new SqlParameter[sqlParameterList.Count];
for( int i=0;i<sqlParameterList.Count;i++)
{
prams[i]=(SqlParameter)sqlParameterList[i];
}
return prams;
}
3、 填充数据实体:
private Product_Info FillInfo(SqlDataReader r)
{
Product_Info product_Info = new Product_Info();
product_Info.ReleaseDate=DateTime.Parse(r["ReleaseDate"].ToString().Trim());
product_Info.TypeID=int.Parse(r["TypeID"].ToString().Trim());
product_Info.Author=r["Author"].ToString().Trim();
return product_Info;
}
这个私有方法就是把从数据库里读到的数据一条条赋给数据实体。这个私有方法会被一个返回数据集的公共方法调用。
4、 四种操作:添加、修改、删除和检索
这几个方法大同小异,代码也显得非常简单。
1)、添加/修改是一个方法,通过关键ID来判断具体是哪一个操作
{
int id=-1;
SqlParameter[] prams=prepareParms(product_Info);
try
{
int retval = RunProc("AddEdit_Product_Info", prams);
if(retval == 0)
{
id=(int)prams[0].Value;
}
return id;
}
catch
{
return id;
}
finally
{
Close();
Dispose();//Dispose Database
}
}
2)、检索 检索有两种返回类型,一种是DataSet,另外一种是数据集。
{
SqlDataAdapter dataAdapter = null;
SqlParameter[] prams=prepareParms(product_Info);
RunProc("Search_Product_Info", prams, out dataAdapter);
DataSet dataSet = new DataSet();
dataAdapter.Fill(dataSet,"table");
dataAdapter.Dispose();
return dataSet;
}
public bool SearchInfoList(Product_Info product_Info)
{
SqlParameter[] prams=prepareParms(product_Info);
SqlDataReader dataReader=null;
try
{
RunProc("Search_Product_Info", prams,out dataReader);
members= new ArrayList();
while(dataReader.Read())
{
members.Add(this.FillInfo(dataReader));
}
return true;
}
catch
{
return false;
}
finally
{
if (!dataReader.IsClosed)
dataReader.Close();
Close();
Dispose();//Dispose Database
}
}
5、 删除方法
{
SqlParameter[] prams=prepareParms(product_Info);
try
{
int retval = RunProc("Delete_Product_Info", prams);
if(retval == 0)
{
return true;
}
else
{
return false;
}
}
catch
{
return false;
}
finally
{
Close();
Dispose();//Dispose Database
}
}
4、存储过程:
通用存储过程的编写
至些一个完整的架构思想便阐述完毕。
这也只是我的一点点想法,并不完善,我也会在后续的项目实施中改善加强,另外所有的抽象过程都是为了让程序简洁通用,而且方便用自动化工具实现,到目前为止,以上所有的部分都已经自动化实现,所以构建底层变得非常轻松。
注:这里的解决方案其实是我综合两个项目的解决方案得出来的,其中一个是论坛式的社区,有一百多万的帖子,采用了Cache机制,性能也满好的。通用架构带来的是高效,但通常都是以一定的性能为代价的。包括Duwamish和Petshop的写法未必是最优的,但一定是最有效和最稳定的。