实战揭密:开发.Net平台应用系统框架

微软的.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:对象-关系数据库映射服务

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值