微软的.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是我们开发的 对象-实体 映射语言),用于确定实体类的结构。例如,一个关于订单的实体类的定义可能类似于下面的结构:
<?xml version="1.0" encoding="gb2312" ?>
<Entity>
<EntityTypeName>Product</EntityTypeName>
<TableName>Product</TableName>
<Columns>
<Column Name="ProductID" DataType="System.String" IsKey="true">
</Column>
<Column Name="ProductName" DataType="System.String" IsKey="false">
</Column>
<Column Name="ProductTypeID" DataType="System.String" IsKey="false">
</Column>
<Column Name="CurrentCount" DataType="System.Decimal" IsKey="false">
</Column>
<Column Name="UnitName" DataType="System.String" IsKey="false">
</Column>
</Columns>
<RefTable Type="Parent">
<TableName>ProductType</TableName>
<ForeignKey>ProductTypeID</ForeignKey>
<PrimaryKey>ProductTypeID</PrimaryKey>
<Columns>
<Column Name="ProductTypeID" DataType="System.String" IsKey="true">
</Column>
<Column Name="ProductTypeName" DataType="System.String" IsKey="false">
</Column>
</Columns>
</RefTable>
<Sqls>
<Sql Name="InsertProduct">
<String>
INSERT INTO Product (
ProductID, ProductName, ProductTypeID, CurrentCount, UnitName
) VALUES(
@ProductID,
@ProductName,
@ProductTypeID,
@CurrentCount,
@UnitName
)
</String>
<Param Name="@ProductID" Column="ProductID">
</Param>
<Param Name="@ProductName" Column="ProductName">
</Param>
<Param Name="@ProductTypeID" Column="ProductTypeID">
</Param>
<Param Name="@CurrentCount" Column="CurrentCount">
</Param>
<Param Name="@UnitName" Column="UnitName">
</Param>
</Sql>
<Sql Name="UpdateProduct">
<String>
UPDATE Product
SET
ProductName=@ProductName
, ProductTypeID=@ProductTypeID
, CurrentCount=@CurrentCount
, UnitName=@UnitName
WHERE
ProductID =@ProductID</String>
<Param Name="@ProductName" Column="ProductName">
</Param>
<Param Name="@ProductTypeID" Column="ProductTypeID">
</Param>
<Param Name="@CurrentCount" Column="CurrentCount">
</Param>
<Param Name="@UnitName" Column="UnitName">
</Param>
<Param Name="@ProductID" Column="ProductID">
</Param>
</Sql>
<Sql Name="DeleteProduct">
<String>
DELETE FROM Product
WHERE
ProductID =@ProductID</String>
<Param Name="@ProductID" Column="ProductID">
</Param>
</Sql>
<Sql Name="SelectByIDProduct">
<String>
SELECT
ProductID, ProductName, ProductTypeID, CurrentCount, UnitName
,ProductType.ProductTypeID
,ProductType.ProductTypeName
FROM
Product
,ProductType
WHERE
ProductID =@ProductID
AND ProductTypeID=ProductType.ProductTypeID</String>
<Param Name="@ProductID" Column="ProductID">
</Param>
</Sql>
</Sqls>
</Entity>
3) 实体对象的结构由一系列的类构造器在运行时刻,根据上述规范制定的XML来生成。这些类构造器实现IClassBuilder接口。我们在系统核心类库中预定义了一些标准的Builder,一般情况下,直接使用这些标准的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:对象-关系数据库映射服务