微软的.Net平台给应用程序开发提供了一个非常好的基础系统平台,但是,如何在这个系统平台上构建自己的应用系统,还需要我们针对应用系统的特点,构建自己的应用系统框架(Framework)。我们在应用.Net开发系统的过程中,结合多年的开发经验,也参考了J2EE的架构,设计了一套.Net下的应用系统框架,以及相应的中间件和开发工具,已经在多个项目中和软件产品中应用,取得了很好的效果。现在向代价介绍这个框架的整体解决方案,希望对您有所帮助。
我们知道,对于典型的三层应用系统来说,通常可以把系统分成以下三个层次:
· 数据库层
· 用户界面层
· 应用服务层
对于应用系统来说,在这三个层次中,系统的主要功能和业务逻辑在应用服务层进行处理,对于系统框架来说,主要处理的也是这个层次的架构。
对于应用服务层来说,在一个面向对象的系统中,以下几个方面的问题是必须要处理的:
· 数据的表示方式,也就是实体类的表示方式,以及同数据库的对应关系,即所谓的O-R Map的问题。
· 数据的存取方式,也就是实体类的持久化问题,通常采用数据库来永久存储数据实体,这就需要解决同数据库的交互问题。这个部分要完成的功能,就是将数据实体保存到数据库中,或者从数据库中读取数据实体。同这个部分相关的,就是对数据访问对象的使用。在框架中,我们对ADO.Net又做了一层封装,使其使用更加简便,同时也统一了对ADO.Net的使用方式。
· 业务逻辑的组织方式。在面向对象的系统中,业务逻辑是通过对象间的消息传递来实现的。在这个部分,为了保证逻辑处理的正确性和可靠性,还必须支持事务处理的能力。
· 业务服务的提供方式。为了保证系统的灵活性和封装性,系统必须有一个层来封装这些业务逻辑,向客户端提供服务,同时作为系统各个模块间功能调用的接口,保证系统的高内聚和低耦合性。这里的客户指的不是操作的用户,而是调用的界面、其他程序等。Web层(ASP.Net页面)通常只同这个部分交互,而不是直接调用业务逻辑层或者数据实体的功能。
为了能够很好的解决这些问题,我们设计了这个框架。在框架中,针对以上问题,我们将应用服务层又划分成五个层次:数据实体层、实体控制层、数据访问层、业务规则层和业务外观层。各个层次同上述问题的关系可以用表表示如下:
层次 |
问题 |
数据实体层 |
数据的表示方式 |
实体控制层 |
数据的存取方式 |
数据访问层 |
提供对数据库的访问,封装ADO.Net |
业务规则层 |
业务逻辑的组织方式 |
业务外观层 |
业务服务的提供方式 |
整个系统的结构图如下:
图中的箭头表示使用关系
将系统划分成这么多层次,其好处是能够使得系统的架构更加清晰,这样每个层次完成的功能就比较单一,功能的代码有规律可循,也就意味着我们可以开发一些工具来生成这些代码,从而减少代码编写的工作量,使得开发人员可以将更多的精力放到业务逻辑的处理上。正是基于这个想法,我们同时开发了针对这个框架的开发工具,并在实际工作中减少很多代码的编写量,效果非常好。同时,为了应用服务层更好的工作,我们设计了一个支持这个框架的应用系统中间件。(现在,已经有多家其他公司在试用这个中间件系统。)
同J2EE的EntityBean不同的是,我们采用了数据实体和实体控制分开的设计方法,这样的做法会带来一定的好处。
下面我将各个部分的设计方案和策略详细介绍如下:
数据实体层
我们首先需要解决的是数据的表示方式的问题,也就是通常的O-R Map的问题。
O-R Map通常的做法是将程序中的类映射到数据库的一个或多个表,例如一个简单的Product类:
public class Product
{
string ProductID;
string ProductName;
float Account;
}
|
在数据库中可能对应了一张Product表:
字段名 |
数据类型 |
ProductID |
Varchar(40) |
ProductName |
Varchar(50) |
Account |
float |
这是最通常的做法,但是,由这种方式会带来一些问题。首先就是数据实体在数据库和程序中的表现方式不一样,对于一些涉及到多个表的“粗粒度对象”,一个实体类可能会引用到多个其它实体类,也就是说会在涉及到对象粒度的建模方面带来一些问题;其次在同数据库交互时,也涉及到一个转换的问题,如果一个对象涉及到对多个表的操作,问题就更大;最后,当系统做查询操作,需要返回多个对象时,因为涉及到转换的问题,效率就比较低下,而如果采用直接返回数据集的方式,虽然能够提高效率,又会带来数据表达方式不一致的问题。
考虑到上述问题,我们在数据实体的表现上采用了另外一种方式,那就是利用DataSet。DataSet是微软在ADO.Net中新提出的数据对象,同ADO的Recordset不同的是,他能够容纳多个记录集。DataSet类似于一个内存数据库,由多个DataTable组成,而一个DataTable又有多个Column。这样的结构,使得他可以同数据库很好的进行映射。同时,我们吸取了J2EE架构中CMP使用XML文件定义实体类结构的优点,采用了类似的解决方案。
因此,在这个方面我们是这样来进行处理的:
1) 核心类库定义了EntityData类,这个类继承了DataSet,添加了一些方法,用来作为所有实体类的框架类,定义了各个实体类的一般结构,至于每个实体类具体的结构,在运行时刻由下述办法确定:
2) 实体类的定义通过XML文件来确定,该XML文件符合JIXML对象实体描述语言的规范(注:JIXML是我们开发的 对象-实体 映射语言),用于确定实体类的结构。例如,一个关于订单的实体类的定义可能类似于下面的结构:
3) 实体对象的结构由一系列的类构造器在运行时刻,根据上述规范制定的XML来生成。这些类构造器实现IClassBuilder接口。我们在系统核心类库中预定义了一些标准的Builder,一般情况下,直接使用这些标准的Builder就可以了。
类构造器采用的类构造工厂的设计模式,如果使用者觉得标准的Builder不能满足要求,也可以扩展IClassBuilder接口,编写自己的类构造器,然后在系统配置文件中指明某各类的类构造器的名称即可。
IClassBuilder的定义如下:
public interface IClassBuilder
{
EntityData BuildClass(string strClassName); //获取类数据结构
SqlStruct GetSqlStruct(string strClassName,string strSqlName);
}
|
这个部分的结构可以用类图表示如下:
当使用者需要某个实体类的时候,只要采用如下语句:
EntityData entity=EntityDataManager.GetEmptyEntity("Product");
|
EntityDataManager的GetEmptyEntity方法通过调用ClassBuilder的BuildClass来实现,并且实现对象的缓存功能。
ClassBuilder的BuildClass方法实现如下:
public EntityData BuildClass(string strClassName)
{
IClassBuilder
builder=ClassBuilderFactory.GetClassBuilder(strClassName);
return builder.BuildClass(strClassName);
}
|
这儿综合使用了Builder和Factory的设计模式。ClassBuilderFactory的作用是根据实体类的名称,读取配置文件中相应的类构造器的具体类名,并返回具体的类构造器。
配置文件ClassBuilders.xml的结构很简单:
<?xml version="1.0" encoding="gb2312" ?>
<Builders>
<Class Name="ProductType"
BuilderName="Jobsinfo.SingleTableClassBuilder" />
</Builders>
|
如果没有为某个实体类指明具体的Builder,系统将调用默认的Builder来构造实体对象的结构。
系统同时提供了实体对象缓存服务。通过上述方式产生的实体对象可以被缓存,这样,在第二次调用该对象时,可以从缓存中读取,而不用从头重新生成,从而大大提高了系统的性能。
在实际的开发过程中,我们感觉到,数据实体层采用这种设计模式具有以下优点:
· 实体类定义XML文件可以通过工具来自动生成,减轻开发工作量。
· 在执行查询操作时,不论是返回一个实体,还是多个实体,数据的表现方式都一样,都是EntityData,而不存在如上面所述的单个对象和数据集的表现方式不统一的问题。
· 在修改实体类的定义时,如果修改的部分不涉及到业务逻辑的处理,只需要修改XML文件就可以了,不用修改其它程序和重新编译。
· 系统提供的实体对象缓存服务可以大大提高了系统的性能。
· 类构造工厂的设计模式大大提高了系统的灵活性。
实体控制层
解决和O-R Map的问题,需要考虑的就是实体类的持久性问题了,也就是同数据库的交互问题。实体控制层用于控制数据的基本操作,如增加、修改、删除、查询等,同时为业务规则层提供数据服务。
实体控制层的类实现IEntityDAO接口。这个接口定义了实现数据操纵的主要必要方法,包括增加、修改、删除和查找。IEntityDAO的定义如下:
public interface IEntityDAO : IDisposable
{
void InsertEntity(EntityData entity);
void UpdateEntity(EntityData entity);
void DeleteEntity(EntityData entity);
EntityData FindByPrimaryKey(object strKeyValue);
}
|
可以看到,这个接口同J2EE中EntityBean的接口定义很象,实际上,我们也是参考了EntityBean的解决方案。
下面是一个Product的DAO类的例子:
public class ProductEntityDAO: IEntityDAO
{
private DBCommon db; //这是数据库访问的类
public ProductEntityDAO()
{
db=new DBCommon();
db.Open();
}
public ProductEntityDAO(DBCommon cdb)
{
this.db=cdb;
}
// 插入一个实体
public void InsertEntity(EntityData entity)
{
CheckData(entity);
db.BeginTrans();
try
{
foreach(DataRow row in
entity.Tables["Product"].Rows)
db.exeSql(row,SqlManager.GetSqlStruct("Product","InsertProduct"));
db.CommitTrans();
}
catch(Exception e)
{
db.RollbackTrans();
throw e;
}
}
//修改一个实体类
public void UpdateEntity(EntityData entity)
{
CheckData(entity);
db.BeginTrans();
try
{
foreach(DataRow row in
entity.Tables["Product"].Rows)
if(row.RowState!=DataRowState.Unchanged)
db.exeSql(row,SqlManager.GetSqlStruct("Product","UpdateProduct"));
db.CommitTrans();
}
catch(Exception e)
{
db.RollbackTrans();
throw e;
}
}
//删除一个实体类
public void DeleteEntity(EntityData entity)
{
CheckData(entity);
db.BeginTrans();
try
{
foreach(DataRow row in
entity.Tables["Product"].Rows)
db.exeSql(row,SqlManager.GetSqlStruct("Product","DeleteProduct"));
db.CommitTrans();
}
catch(Exception e)
{
db.RollbackTrans();
throw e;
}
}
//查找实体类
public EntityData FindByPrimaryKey(object KeyValue)
{
EntityData entity=new EntityData("Product");
SqlStruct
sqlProduct=SqlManager.GetSqlStruct("Product","SelectByIDProduct");
db.FillEntity(sqlProduct.SqlString,sqlProduct.ParamsList[0],
KeyValue,entity,"Product");
return entity;
}
public EntityData FindAllProduct()
{
EntityData entity=new EntityData("Product");
SqlStruct
sqlProduct=SqlManager.GetSqlStruct("Product","FindAllProduct");
db.FillEntity(sqlProduct.SqlString,null,null,entity,"Product");
return entity;
}
// 校验数据数据输入的有效性
private void CheckData(EntityData entity)
{
if(entity.Tables["Product"].Rows[0]["ProductID"].ToString().Length>40)
throw new ErrorClassPropertyException
("Property ProductID should be less than 40 characters");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(true);
}
protected virtual void Dispose(bool disposing)
{
if (! disposing)
return; // we're being collected,
so let theGC take care of this object
db.Close();
}
}
|
同数据实体层相结合,这两部分实现了应用服务层同数据库的交互。这两个部分结合,完成了类似于J2EE中EntityBean的功能。
采用数据实体和实体控制分开的设计方法,具有以下优点:
· 避免了J2EE体系中操纵EntityBean系统资源消耗大,效率低下的缺陷。
· 解决了J2EE体系中使用EntityBean传输数据时开销大,过程复杂、效率低的缺陷。
· 可以单独修改实体结构和对实体数据的操纵,使得系统更加灵活
· 数据实体的XML定义文件和实体控制层的类可以通过工具自动生成,减轻开发工作量。
数据访问层
为了为实体控制层提供对数据库操作的服务,我们设计了这个部分。这个层次通常执行以下一些操作:
· 连接数据库
· 执行数据库操作
· 查询数据库,返回结果
· 维护数据库连接缓存
· 数据库事务调用
为了统一对数据的访问方式,我们在设计的时候,在框架的类库中包含了数据访问服务,封装了常用的对各种数据库的操作,可以访问不同类型的数据库,这样,在具体软件系统开发的时候,可以不用考虑同数据库的连接等问题,也使得应用系统在更换数据库时,不用修改原有的代码,大大简化了开发和部署工作。数据访问服务还维护数据库连接缓存,提高系统性能,以及对数据库事务调用的服务。
数据访问服务在核心类库中主要通过DBCommon类来提供对数据访问功能调用的服务。DBCommon的使用方法在上面的ProductEntityDAO中可以看出一二。更多的可以看看Demo工程中的使用。
业务规则层
业务规则层需要完成的功能是各种业务规则和逻辑的实现。业务规则完成如客户帐户和书籍订单的验证这样的任务。这是整个应用系统中最为复杂的部分,没有太多的规律可循。但是,我们在完成上面的工作后,对于这个部分的开发,也可以起到一定的简化的工作。这从下面的例子可以看到。
业务规则层的设计通常需要进行很好的建模工作。业务规则的建模,一般采用UML来进行。可以使用UML的序列图、状态图、活动图等来为业务规则建模。这个部分的工作,通常通过一系列的类之间的交互来完成。
业务规则通常要求系统能够支持事务处理(Transaction)。在这个地方,.Net提供了很方便的调用Windows Transaction Server的手段。关于这个部分的内容,各位自己阅读MSDN就非常清楚了,这里就不做详细的介绍了。
例如,在一个库存系统的入库单入库操作中,除了需要保存入库单外,在这个之前,还必须对入库单涉及的产品的数量进行修改,其代码通常如下(使用了事务处理):
public void StoreIntoWarehouse(EntityData IndepotForm)
{
DataTable tbl=IndepotForm.Tables["InDepotFormDetail"];
try
{
ProductEntityDAO ped=new ProductEntityDAO();
for(int i=0;i<tbl.Rows.Count;i++)
{
DataRow formdetail=tbl.Rows[i];
string productID=formdetail["ProductID"].ToString();
decimal
inCount=(decimal)formdetail["InCount"];
EntityData product=ped.FindByPrimaryKey(productID);
DataRow productRow=product.GetRecord("Product");
productRow["CurrentCount"]=(decimal)productRow["CurrentCount"]+inCount;
ped.UpdateEntity(product);
}
ped.Dispose();
InDepotFormEntityDAO inDepotForm=new
InDepotFormEntityDAO();
inDepotForm.InsertEntity(IndepotForm);
IndepotForm.Dispose();
ContextUtil.SetComplete();
}
catch(Exception ee)
{
ContextUtil.SetAbort();
throw ee;
}
}
|
业务外观层
业务外观层为 Web 层提供处理、浏览和操作的界面。业务外观层用作隔离层,它将用户界面与各种业务功能的实现隔离开来。
业务外观层只是将已经完成的系统功能,根据各个模块的需要,对业务规则进行高层次的封装。
框架没有规定采用在业务外观层采用何种实现方式,但是建议使用Web Service来提供服务。采用IIS为Web服务器,可以很方便的部署Web Service。
· Web层
Web 层为客户端提供对应用程序的访问。Web 层由 ASP.NET Web 窗体和代码隐藏文件组成。Web 窗体只是用 HTML 提供用户操作,而代码隐藏文件实现各种控件的事件处理。
通常,对于数据维护类型的ASP.NET Web 窗体和控件事件处理代码,我们提供了工具来生成,减轻开发工作量。
除了上述6个逻辑层以外,系统通常还包括一个系统配置项目,提供应用程序配置和跟踪类。
框架服务的设计策略
为了能够很好的支持上面所述的系统架构,我们需要一套核心的类库,以实现对构筑其上的应用软件的支持。这样,在具体每个应用系统的开发时,可以省略很多基础性的工作,提高开发的效率。在这个方面,我们设计了以下核心类和接口:
· EntityData:定义实体类的通用结构
· IClassBuilder:定义实体类结构构造的结构。我们预定义了根据这个接口实现的几个标准类:AbstractClassBuilder、SingletableClassBuilder、ThickClassBuilder、StandardClassBuilder。这些Builder通过ClassBuilderFactory进行管理。
· IEntityDAO:定义实体控制类的接口
· EntityDataManager:提供对所有实体类的缓存管理和查找服务
· DBCommon:封装数据库操作
· ApplicationConfiguration:记录系统配置
· SqlManager:管理系统的SQL语句及其参数。
通过这些核心的类和接口,框架能够为应用系统提供如下服务:
· O-R Map:对象-关系数据库映射服务
这部分完成应用程序中的实体对象同关系型数据库的映射,主要为数据实体层提供服务。
在这个部分中,定义了JIXML实体-对象映射语言。这是我们开发的一种使用XML来描述对象-实体间的映射关系的规范语言,开发者可以使用它来描述对象-实体间的映射关系。开发者也可以直接扩展IClassBuilder接口,手工完成对象-实体间映射关系的代码。系统在运行时刻,会根据配置文件的设置,调用实体类的构造器,动态构造出实体对象的结构。
· Database Access:数据库访问服务
这个部分提供对数据库访问的服务。在这个框架上构建的应用软件系统,不直接操纵数据库,而是通过类库提供的数据访问服务来进行。数据库访问服务作为应用程序同数据库之间的中介者,能够有效防止对数据库的不安全操作。
数据库访问服务同时提供了对数据库库事务处理的调用方法,开发者可以很方便的通过数据库访问服务调用数据库的事务处理功能。
· DML Search:数据操纵语句查询服务
在系统架构中,对数据库进行操作的SQL语句不在程序中硬编码,而是同数据实体层的实体类结构一样在XML文件中描述,其结构符合JIXML规范。这些操纵语句中的基本部分,如数据的插入、删除、修改、查询等语句,可以通过我们自己开发的工具生成。这样,在系统的便于修改性和灵活性上能够得到很大的提高。这样一来,系统必须提供这些数据操纵语句的查询服务。核心类库提供了在XML文件中查找这些数据操纵语句和相关参数的服务。
· Entity Buffer&Search:实体对象缓存&查找服务
系统中的实体对象在第一次创建后,就被系统缓存起来,当系统第二次需要访问该对象时,不需要再从头创建这个对象,而只需要从缓存中取出即可。这就是框架提供的实体对象缓存服务。同这个服务相关联的是实体对象的查找服务,即从这些缓存的实体对象中寻找相应的实体对象的服务。
· Transaction:事务处理服务
我们充分利用Windows COM+事务处理机制的强大功能,使在应用程序能够充分使用事务处理的功能,保证应用系统的稳定性和可靠性。
当某个类需要使用事务处理功能时,首先使该类继承System.EnterpriseServices名称空间下的ServicedComponent类,然后使用如下方式申明该类使用的事务类型:[Transaction(TransactionOption.Required)]。系统在该类第一次被调用时,自动在COM+服务中注册中该类,使得应用程序可以使用COM+的事务处理功能。
系统支持如下几种事务处理类型:
成员名称 |
说明 |
Disabled |
忽略当前上下文中的任何事务。 |
NotSupported |
使用非受控事务在上下文中创建组件。 |
Required |
如果事务存在则共享事务,并且如有必要则创建新事务。 |
RequiresNew |
使用新事务创建组件,而与当前上下文的状态无关。 |
Supported |
如果事务存在,则共享该事务。 |
同时,为了简化开发,我们还为这个框架设计了一个开发工具,并且作为插件集成到VS.Net的开发环境中,能够大大减少开发的代码编写工作量。
这样,通过以上这些工作,我们达到了以下目标:
· 有了一个非常清晰的系统架构
· 因为有了一套核心的类库来为应用系统提供服务,使得我们在后面的开发过程中可以减少很多基础性的工作
· 有了自己的有针对性的开发工具,能够减少大量的重复编码的工作。
为了读者能够更好的了解系统的结构,附上一个Demo工程(下载Demo工程)。这是一个简单的仓库入库的示例,基本上展示了这个框架的应用系统的结构和核心类库的使用。
不得不看得实战内容:利用.Net框架开发应用系统
作者简介:孙亚民,1998年毕业于南京大学,现任苏州迪讯软件开发有限公司技术总监,熟悉J2EE架构、.Net以及C#语言。
|