UML软件工程组织

利用.Net框架开发应用系统
作者:孙亚民    本文选自:赛迪网
《开发.Net平台应用系统框架》(下面简称《一》文)中,我介绍我们开发的一个.Net下应用软件系统的框架,在本文,我将介绍我们是怎么在这个框架下开发系统的。前文附带了一个logistics示例工程,这是我们从开发的物流管理系统中精简出来的一个例子,为了便于说明,现在就按照这个简化的工程来具体谈谈各个部分的设计策略和框架的使用,从中也可以管窥整个系统的设计模式。

这个工程主要功能是入库单的入库,以及相应的产品资料维护的辅助功能。因为工程的功能比较一目了然,而本文的主要目的是说明软件的设计策略,因此,对于涉及到需求的Use Case等内容就不会加以论述了。

静态建模部分(数据实体层的设计)


在这个案例中,主要涉及到产品和入库单两个对象,当然,还有一些辅助对象,如入库单的明细等。整个部分的静态模型可以用类图表示如下:



相应的,数据库的设计如下:



对象的粒度

现在,我们要做详细的实体类的设计了,也就是将数据库映射到程序的实体类中。

在考虑实体对象的设计时,“对象的粒度”是一个需要仔细考虑的问题,实体对象按照对象的粒度,通常可以分成所谓的“粗粒度对象”和“细粒度对象”。在J2EE中使用EntityBean为实体类作设计时,我们总是尽量将实体对象建模成粗粒度的对象,因为EntityBean是非常耗费资源的,系统中如果存在大量的细粒度对象,会在很影响系统的性能。而在J2EE中,粗粒度对象的设计也是一个需要一定技巧的工作。

在我们设计的框架中,由于在O-R Map部分充分利用了DataSet的强大功能,使得不论是粗粒度对象,还是细粒度对象,我们都采用了相同的处理方式,在对象粒度的设计方面能够得到一定的简化,对象粒度的粗细也不会对系统性能造成太大的影响。

在这几具体案例中,很明显,对于ProductType和Product的结构比较简单,在对他进行操作时,我们通常只涉及到一张表。虽然,对于Product来说,在对其进行查询的时候,需要知道产品类型的时候,会涉及到ProductType表,但是,在大部分的增加、修改、删除的操作中,我们都只对Product表进行操作,因此,我们把这两个对象都设计成细粒度对象。Product类的XML描述文件如下(删去了Sql语句部分):


Product.xml
<?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>
</Entity>
与Product不同的是,入库单是一个相对复杂的对象,他包含了自身的一些信息,还有一些明细资料。实际上,入库单的明细也是入库单的一个组成部分,当我们在处理入库单的时候,我们总是会同时处理入库单的明细,入库单和他的明细是不可分的。如果采用传统的O-R Map方法来处理入库单(包含明细),我们通常没有办法用一个简单的类来对其进行描述,在采用我们的这个框架来处理时,我们同样也不能在DataSet中用一个单独的DataTable来描述,同时,在处理入库单时,我们在数据库中也会涉及到多个表的操作。在这种情况下,我们将入库单连同他的明细一起设计成一个“粗粒度对象”。所幸的是,这种设计同细粒度对象的设计差别不大。读者可以仔细比对两个XML文件的不同,了解对于不用粒度的对象的设计策略。


InDepotForm.xml
<?xml version="1.0" encoding="gb2312" ?>
<Entity>
    <EntityTypeName>InDepotForm</EntityTypeName>
    <TableName>InDepotForm</TableName>
    <Columns>
        <Column Name="InDepotID" DataType="System.String" IsKey="true"></Column>
        <Column Name="InDepotTime" DataType="System.DateTime" IsKey="false"></Column>
    </Columns>
    <RefTable Type="Child">
        <TableName>InDepotFormDetail</TableName>
        <ForeignKey>InDepotID</ForeignKey>
        <PrimaryKey>InDepotID</PrimaryKey>
        <Columns>
            <Column Name="InDepotDetailID" DataType="System.String" IsKey="true"></Column>
            <Column Name="InDepotID" DataType="System.String" IsKey="false"></Column>
            <Column Name="ProductID" DataType="System.String" IsKey="false"></Column>
            <Column Name="InCount" DataType="System.Decimal" IsKey="false"></Column>
        </Columns>
</RefTable>
</Entity>


这样,在运行的过程中,我们通过如下方式得到一个入库单对象时,在entity实例对象中,实际上包含了InDepotForm和InDepotFormDetail两个DataTable。


EntityData entity=EntityDataManager.GetEmptyEntity("InDepotForm ");


这个部分的实体描述的XML文件,都是通过我们开发的工具生成的,因此,在开发的过程中,在这个部分,我们没有花太多的代码编写时间,这些XML文件,在数据库设计完成后,几乎是一夜间就完成了。

数据访问部分(实体控制层的设计)


解决了数据实体的设计问题后,我们就要处理同数据库的交互部分。这个部分的类实现IEntityDAO接口。这个接口在《一》文中已经做了介绍。这个部分的代码是很有规律的,我们设计的工具能够生成所有基本的增、删、改、查的方法,当然,对于一些复杂的查询方法,还是需要自己再补充的。在《一》文中,我已经介绍了产品的数据访问的类的结构,下面再看一下入库单的数据访问类的结构,应该对这个部分的结构有一个更好的理解,毕竟代码是最能够说明问题的。因为代码比较长,所以只保留了新增入库单的代码,其余的代码各位可以在示例工程中找到。


public class InDepotFormEntityDAO: IEntityDAO
{
private DBCommon db;  //数据访问对象
……//略去构造函数部分。这个部分会建立数据库的连接。
// 插入一个实体
public void InsertEntity(EntityData entity)
{
	CheckData(entity);
	db.BeginTrans();
	try
	{
		//插入父表内容
foreach(DataRow row in entity.Tables["InDepotForm"].Rows)
db.exeSql(row,SqlManager.GetSqlStruct("InDepotForm","InsertInDepotForm"));
		//插入子表内容。如果是细粒度对象,则不会有这段代码。
	foreach(DataRow row in entity.Tables["InDepotFormDetail"].Rows)
db.exeSql(row,SqlManager.GetSqlStruct("InDepotForm","InsertInDepotFormDetail"));
		//如果没有错误,完成事务处理
db.CommitTrans();
		}
	catch(Exception e)
	{
		//否则,回滚事务
db.RollbackTrans();
		throw e;
	}
	}
    //修改实体
public void UpdateEntity(EntityData entity){//代码略}
	//删除实体
public void DeleteEntity(EntityData entity){//代码略}		
 	//查找实体
public EntityData FindByPrimaryKey(object KeyValue)	{//代码略	}
 	// 校验数据数据输入的有效性
	private void CheckData(EntityData entity){//代码略}

		…….//略去其余代码
	}
业务逻辑的处理


有了上面的基础,我们很容易将这些类进行组合,构建我们的业务处理功能。在这个系统中,涉及到复杂业务处理的部分只有入库单入库这个功能,这个功能我们封装在Wharehouse类中,其过程可以用序列图表示如下:



相应的程序代码和注解如下,在这里,我们使用了事务处理,因此,Wharehouse类继承了System.EnterpriseServices.ServicedComponent:


//设置事务处理类型
	[Transaction(TransactionOption.Required)]
//继承ServicedComponent,以支持使用Windows的Transaction Service
	public class Wharehouse : System.EnterpriseServices.ServicedComponent 
	{
		public Wharehouse(){}
		//入库的业务逻辑代码
		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;
			}
		}
	}


业务服务的提供


现在,整个系统的功能部分已经完成了,我们需要将这些功能组装成系统的各个模块,以便客户端的调用。

在前面的开发过程中,我们实际上还没有对系统进行明确的功能模块划分,在这里,我们才开始所谓的模块划分。在所有客户端对系统的调用中,基本上都是调用这个部分的功能,而不会直接调用前面几个部分的内容,这样使系统达到良好的封装性。

这是一个很好的软件开发的方式。采用这种做法,我们可以将前面的内容封装成一个个的组件,在这里,可以根据需要很方便的利用前面的组件进行功能的重新组合,也方便系统的修改和升级,为软件开发的组件化奠定基础。

在本系统中,业务服务位于BusinessFacade目录,在这里,我们将系统分成两个模块:产品资料的维护和仓库事务管理,模块功能调用接口分别封装在ProductManagement和WharehouseManagement类中。这两个类的代码很简单,主要是封装前面几层内容的功能。例如,WharehouseManagement类封装了入库的操作供客户端掉用,他的代码一目了然,如下:


//类设计成sealed,不能被继承
public sealed class WharehouseManagement
	{
	//采用Singleton设计模式,私有的构造函数,使得类不能直接实例化,
//只能调用静态的StoreProductIntoWharehouse方法。
private WharehouseManagement()
	{
	}
	public static void StoreProductIntoWharehouse(EntityData entity)
	{
		Wharehouse house=new Wharehouse();
		house.StoreIntoWarehouse(entity);
		}
	}


WEB层的设计


1、 WEB层的主要功能是同客户交户,这一层向用户提供服务,主要功能是提供HTML界面,接受用户的输入,调用业务功能等,完成用户的需求。在这个层次里面没有业务逻辑的处理,而只是调用业务层面提供的服务。下面看看一个入库操作的例子。


private void btnAdd_Click(object sender, System.EventArgs e)
	{
		//得到一个InDepotForm实例
EntityData entity=EntityDataManager.GetEmptyEntity("InDepotForm");
		DataRow row=entity.GetNewRecord("InDepotForm");
		//设置入库单主信息
		row["InDepotID"]=valInDepotID.Text;
	row["InDepotTime"]=System.DateTime.Parse(valInDepotTime.Text);
		entity.AddNewRecord(row,"InDepotForm");
		//设置入库单明细信息,这是从DataGrid中读取得
DataTable detail=entity.Tables["InDepotFormDetail"];
		for(int i=0;i<gridDetail.Items.Count;i++)
		{
			DataRow rowdetail=detail.NewRow();
			rowdetail["InDepotID"]=valInDepotID.Text;
	rowdetail["InDepotDetailID"]=gridDetail.Items[i].Cells[0].Text;
	rowdetail["ProductID"]=gridDetail.Items[i].Cells[1].Text;
rowdetail["InCount"]=decimal.Parse(gridDetail.Items[i].Cells[2].Text.Trim());
		detail.Rows.Add(rowdetail);
		}
		//执行入库操作
		WharehouseManagement.StoreProductIntoWharehouse(entity);
		}


在这个层次中,我们没有将表单直接写在ASP.Net页面中,而是先把功能写成Web控件,然后再在ASP.Net页面中引用这些控件。这样做的目的,主要是为了将来的重用性考虑。

开发过程的组织


我们认为,一个好的系统架构,不仅是为了使软件的结构更加清晰,更加有利于修改和重用,而且也应该能够方便团队之间的合作。我们开发的这套系统架构能够很方便团队之间的合作,下面简单介绍一下整个项目的开发过程,供大家参考。当然,这里只是就本项目给大家做一个简介,并不涉及太多的软件工程过程的东西。 

1、项目角色的配置。 

在这个项目中,因为项目的规模和公司的具体情况,我们没有将角色分得太细,我们安排了如下主要角色:系统分析、界面设计、美工、程序员、数据库设计、测试员。 

2、各个阶段参与角色的主要任务 

· 在分析阶段,主要是由系统分析员对系统进行需求的分析和基本建模,美工人员则做一些页面的效果图。 

· 在设计阶段,系统的模型就比较成熟了,在静态模型(类图)完成后,数据库设计人员可以做数据库设计了。系统分析员可以将业务层面需要提供的服务的接口代码原型写出。当然,这时候,这些类的方法都是空的,会在实现阶段填充。界面设计人员可以做页面了,美工会协助将页面美化。当然,界面设计人员最好是懂一点代码的美工,这样就不需要将这个工作分开了。 

· 在实现阶段,主要是程序员实现系统的业务逻辑。因为实体类以及同数据库的交互可以通过我们自己设计的工具很快生成,所以,这个部分的时间会花的很少,主要是处理业务逻辑部分的代码。在这个阶段,界面设计人员也需要将各个功能的页面都完成。当然,这中间会涉及到很多设计修改的工作,这就不是本文论述的范围了。 

因为有了业务层面这个层,所以,界面设计人员和处理业务逻辑的程序员的工作,实际上可以分开,这也有利于团队间的分工协作。 

· 测试阶段的工作没有什么多说的,一般都这样啦。 


同J2EE的一点小小比较


笔者对J2EE和.Net架构都比较清楚,本架构也参考了J2EE架构的实现。个人感觉,J2EE提供了一个很好的应用系统的框架(可能是目前最好的应用系统框架),但是,J2EE的整个模型还是比较复杂的,普通的开发人员熟悉J2EE架构是需要花很多精力的,并且,J2EE对硬件设备的要求比较高,开发效率也不是很高(同一些RAD比较),不适合一些短平快项目的开发。 

微软提供了.Net的基础平台,但是没有像J2EE那样提供一个应用系统开发的“标准”架构。但是,一旦我们确定了自己的开发框架,.Net系统开发效率高的优势就能够充分的体现出来,尤其是对一些中小型项目。 

当然,我说这些话,并没有在J2EE和.Net之间比个高下的意思,只是客观的做一些比较,各位千万不要扔砖头。 


结束语


上面将项目的各个部分的设计思路给大家,希望对大家能够有所帮助。 

作者简介:孙亚民,1998年毕业于南京大学,现任苏州迪讯软件开发有限公司技术总监,熟悉J2EE架构、.Net以及C#语言。 

 

版权所有:UML软件工程组织