一、Duwamish 7.0的架构
Duwamish 7.0是vs.net中微软提供的一个企业级的示例,最近在学设计方面的东西,所以有时间边看边学这个示例。做了一些笔记,和大家一起讨论。
学习Duwamish 7.0,首先要看的当然是它的一个整体的结构式,在msdn自带的帮助文件中,我们看到了它的一个整体的结构,如下图所示:
Duwamish 7.0分为四层,分别为:
l Web层
相当于是用户界面层,直接与用户交互的web窗体,从源码中我们可以看到,它有以下的一些界面页:
页面名称 作用
book.aspx 用以显示图书的详细信息的页面
default.aspx 默认页,显示当天的精选书
categories.aspx 用于分类显示图书的页面,它由两部分组成,上半部分显示当天推荐的该分类的图书信息,下半部分显示该分类的
errorpage.aspx 是一个静态页面,显示一成不变的错误信息
searchresults.aspx 显示搜索结果页面,用了一个datalist控件显示搜索的结果;不支持分页
shoppingcart.aspx 购物车页,用于填写购书的订单,用datagrid控件操作,支持批量修改和更新。不支持删除,设为零时能删除;不用单击update按钮就自动更新了;update按钮用于修改订购书的数量后刷新价格。
viewsource.aspx 既然是示例,当然可以看源代码了,这一页是专门用于查看源代码的
下面的页面是用于管理用户及用户订单系统,微软专门把它放在secure文件夹下:
页面名称 作用
account.aspx 新客户注册页及客户修改个人信息页;
checkout.aspx 确认购买页面,填写收货人的详细地址和联系方式,填入信用卡的信息,列出购买的清单及总的费用信息。
order.aspx 显示用户的订单信息,以供用户打印该订单
在Duwamish 7.0中,大量的运用了用户控件,各个用户控件的功能不一,用户控件统一放在modules文件夹下:
用户控件名称 作用
accountmodule.ascx 对应于account.aspx页面,新客户注册页及客户修改个人信息
bannermodule.ascx 每一页都包含有该用户控件,它定义的页面的头部信息,在页面中看到的头上的哪片黑色的区域就是它了,包含一个图片,三个按钮。
categoriesmodule.ascx 每一页都包含有该用户控件,它显示了书籍的分类信息。在页面的左边的”Browse Categories”文字开始到” Behind The Scenes”文字结束就是该控件的界面内容了
checkoutmodule.ascx 对应于checkout.aspx页,因为checkout.aspx页是一个按步骤操作的页(用panel控件控制),每走一页,checkoutmodule.ascx控件中的箭头就往前走或往后退一格。在父页面中通过控制checkoutmodule.ascx控件的Stage属性来控制
dailypickmodule.ascx 用于显示推荐的图书信息,在default.aspx页和categories.aspx页中用到
searchmodule.ascx 搜索功能控件,每页的搜索功能都由这个控件实现
viewsourcemodule.ascx 查看源码功能控件,每页的查看源功能都是由这个用户控件实现
所有用户界面层就是上述的页面和用户控件,看起来其实也不多。
l 业务外观层
什么是业务外观层,这是四层结构里面新增的东西?有什么用呢?现在我也不知道,先让我们看看Duwamish 7.0中业务层中包含一些什么?打开BusinessFacade项目,这里面的东西就是Duwamish 7.0业务外观层了,看看里面有下面的这些文件组成:
文件名称 类作用
CustomerSystem.cs CustomerSystem类是客户系统的业务外观层,它为客户子系统提供了一个简单的接口,该类支持远程处理的应用程序中跨应用程序域边界访问。它继承自MarshalByRefObject类。从Duwamish 7.0中提供的visio图上看,CustomerSystem类只有三个方法,分别是:GetCustomerByEmail方法(从email和密码获得客户的信息),UpdateCustomer方法(更新客户的信息,接收一个CustomerData对象),CreateCustomer方法(当然是用于创建一个新的客户了)
OrderSystem.cs OrderSystem类用于处理订单的业务外观,它只有两个方法:GetOrderSummary方法(用于统计订单),AddOrder方法(用于新增一个订单)
ProductSystem.cs ProductSystem类用于处理书的业务外观,它的方法比较多,有五个分别是:GetCategories方法(通过分类的id获得类别自身的信息);GetCategoryItems方法(通过分类的id获得该类下的所有的书的信息);GetDailyPickItems方法(通过分类的id获得该类下的推荐书的信息);GetItemById方法(通过书的id获得有关书的信息);GetSearchItems方法(根据指定的检索字段条件以及书名的关键字查询书的信息)
看了上面业务层的类以后,我们发现所有的业务层类都只有方法,没有属性,我的理解是它是所有与用户界面有关的操作的一些方法的定义。
这两天又到微软中国网站上看看,发现了卢彦写的几篇关于Duwamish 7.0的文章其中的一篇就是有关为什么要加业务外观层的,看了后,才完全理解,下面我们来看看卢彦的这篇文章的片断:
在Web应用程序中,有部分操作只是简单的从数据库根据条件提取数据,不需要经过任何处理,而直接将数据显示到网页上,比如查询某类别的图书列表。而另外一些操作,比如计算定单中图书的总价并根据顾客的级别计算回扣等等,这部分往往有许多不同的功能的类,操作起来也比较复杂。我们可以先想象一下,如果我们采用三层结构,这些商业逻辑一般是会放在中间层,那么对内部的这些大量种类繁多,使用方法也各异的,不同的类的调用任务就完全落到了表示层。这样势必会增加表示层的代码量,将表示层的任务复杂化,和表示层只负责接受用户的输入并返回结果的任务不太相称,并增加了层与层之间的耦合程度。
为了解决这个问题,我们先来看看《设计模式》一文中对Facade模式的描述:
意图:
为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
适用性:
当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。Facade可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过Facade层。
客户程序与抽象类的实现部分之间存在着很大的依赖性。引入Facade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
当你需要构建一个层次结构的子系统时,使用Facade模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过Facade进行通讯,从而简化了它们之间的依赖关系。
结构图:
上文提出的这个矛盾,正好和设计模式中Facade模式中所描述的需要解决的问题非常吻合,在《设计模式》中提出的解决的办法就是引入一个Facade对象,让这个Facade来负责管理系统内部类的调用,并为表示层提供了一个单一而简单的接口。这个Facade对象,在我们的Duwamish的设计中,就是BusinessFacade(业务外观)层。
l 业务规则层
业务规则层包含各种业务规则和逻辑的实现,在Duwamish 7.0中业务规则层完成如客户帐户和书籍订单的验证这样的任务。它包含了两个类:
文件名称 类作用
Customer.cs 它有一个私有的常量REGEXP_ISVALIDEMAIL,用于验证客户的输入的email地址是否正确;insert方法用于验证和新建一条客户记录;update方法用于更新某个客户的信息;GetCustomerByEmail私有方法通过email验证是否已存在该客户的信息;Validate私有方法用于验证customer数据是否正确,它通过调用类内的IsValidEmail(验证email方法)和IsValidField(验证字段的内容是否超长字段规定的长度)来验证整个customer对象是否正确
Order.cs 私有的常量MINIMUM_SHIPPING_CHARGE,私有的常量STANDARD_ITEM_COUNT。CalculateTax方法用于计算税收。CalculateShipping用于计算购物的总价格。InsertOrder方法用于插入一个订单;IsValidField用于验证字段的正确性
从上面的两个类设计来看,业务逻辑层的功能是对业务对象是否符合业务逻辑的验证,无需验证的对象则无需写其业务层,从业务外观层我们看到,CustomerSystem.类对应于Customer类,它对CustomerSystem.提交的CustomeData对象进行验证。Order类则对OrderSystem类提交的OrderData对象进行验证。但是我们看到没有对ProductSystem类的验证的对象,这是因为在Duwamish 7.0中没有提供对product(在这里是指书)对象的更新或新增操作。我想,如果它提供了对书的维护功能的话,它也肯定有这个业务规则对象。
原文请看:http://www.microsoft.com/china/community/ ... icle/TechDoc/duwamish.asp
l 数据访问层
数据访问层负责对业务层提供数据操作,也就是它负责和底层的数据库打交道。业务层或者通用层中所有的对象都通过数据访问层的对象访问数据库。数据访问层中的类是按业务对象来组织的,每个业务对象中包含的数据可能存在不同的几种数据表中,它由数据访问类统一组织成一个概念中的对象,它相当于是一个面向对象的数据库层,负责映射面向对象与关系数据库间的关系。
对数据库的所有操作均由存储过程完成,数据层只是在前台调用后台的存储过程。
文件名称 类作用
Books.cs Books类有许多的方法,构造函数Books首先创建了一个SqlDataAdapter对象,然后指定了该对象的SqlConnection对象。Books类实现了Idisposable接口,实现的Dispose方法,用于销毁该对象。下面的各个方法都是根据不同的参数获得BookData,包括:GetBooksByCategorId(对应于同名的存储过程,通过类别id获得bookdata数据集对象),GetDailyPickBooksByCategoryId(对应于同名的存储过程,通过类别id获得推荐的bookdata数据集对象),GetBookById(通过书的id取得书的信息),GetBooksByAuthor(通过作者名获得书的信息,可能有N条记录),GetBooksByISBN(通过isbn获得书的信息),GetBooksBySubject,GetBooksByTitle,上述的方法其它都是通过FillBookData(通过传进行存储过程名作参数和参数值执行SqlDataAdapter对象的fill方法填充bookdata对象)方法执行对应的存储过程来获得BookData。
Categories.cs Categories类的方法比较少,除了构造函数和dispose方法外,就只有GetCategories方法(通过分类的id获得该类的父、本、子三级的分类对象)和FillCategoryData私有方法(为GetCategories方法从底层数据库中获取数据到CategoriesData)。
Customers.cs Customers类除了构造函数和dispose方法外,有三个公开的方法,LoadCustomerByEmail方法(调用GetLoadCommand方法,获得sqlcommand对象后,执行GetCustomerByEmail存储过程,获得customerdata对象),UpdateCustomer方法(更新整个的customer对象,在UpdateCustomer存储过程中又调用了UpdateCustomerAddress存储过程来更新Addresses表。它有一个缺点就是每次更都会完全的更新两张表的所有的内容),InsertCustomer(与UpdateCustomer方法类似,它调用InsertCustomer和InsertAddress两个存储过程完成两个表的插入工作,唯一不同的插入操作是先插入主表,然后再插入从表。更新是先从表,后主表。),及三个私有的为前三个公开的方法服务的方法。GetLoadCommand方法(生成调用GetCustomerByEmail存储过程的sqlcommand命令返回),GetInsertCommand(生成调用InsertCustomer存储过程的sqlcommand命令返回,它用了sqlparameter对象的sourcecolumn属性映射对dataset的某个对象),GetUpdateCommand(它与GetInsertCommand方法类似)。
Orders.cs Orders除了构造函数和dispose方法外,也只有一个公开的方法InsertOrderDetail方法(插入一个订单到数据库,详细的说明看源码注释)及一个私有方法GetInsertCommand(初始化DataAdapter对象的Insert命令参数集)
l 通用层
映射关系数据库表到实际应用的类(对象)层。相当于是一个面向对象的数据库层,把物理的数据库表的字段映射成业务对象:
文件名称 类作用
BookData.cs BookData类继承自dataset类,创建了一个datatable表,用于存储书的数据。支持序列化。(序列化有什么用?它没有声明authors字段,但填充的时却有这个字段?)
CategoryData.cs CategoryData类继承自dataset类,创建了一个datatable表,用于存储书分类的数据。支持序列化。
CustomerData.cs CustomerData类继承自dataset类,创建了一个datatable表,用于存储书的数据。支持序列化。
OrderData.cs OrderData类也继承自dataset类,但它包含了五个表,各个表字段不一样。
l 系统架构层
文件名称 类作用
ApplicationConfiguration
DuwamishConfiguration
太乱了,我把《项目总结》系列和这个打成包,要的,留个email!
三、编程技巧学习
1. 存储过程技巧
1) 输出参数可以当输入参数使用
2) 字符串字段的累加的方法
我们看看GetBookById存储过程的一段源码:
PROCEDURE GetBookById
@BookId INT
AS
-- max size = 10 Authors
DECLARE @AuthorList nvarchar(480)
SET NOCOUNT ON
-- initialize @AuthorList
SELECT @AuthorList = ""
-- build list of authors
SELECT @AuthorList = @AuthorList + a.Name + ", "
FROM Books b,
BookAuthor ba,
Authors a
WHERE b.ItemId = @BookId
AND ba.ItemId = b.ItemId
AND a.PKId = ba.AuthorId
-- remove last comma
SELECT @AuthorList = LEFT(@AuthorList,LEN(@AuthorList) - 1)
把这段单独拷出来,在查询分析器运行,我们发现这段是用来取某本书的作者的列表,一本书可能有多个作者,
SELECT @AuthorList = @AuthorList + a.Name + ", "
FROM Books b,
BookAuthor ba,
Authors a
WHERE b.ItemId = @BookId
AND ba.ItemId = b.ItemId
AND a.PKId = ba.AuthorId
就是把书的作者名一个个取出来,累加到@AuthorList变量中,各个作者用逗号隔开,最后一句SELECT @AuthorList = LEFT(@AuthorList,LEN(@AuthorList) - 1)把最后一个作者后的逗号去掉。
3) 存储过程参数与dataset中字段的映射
在Customers类中的GetInsertCommand方法中,我们看到了下面的代码:
sqlParams[PKID_PARM].SourceColumn = CustomerData.PKID_FIELD;
sqlParams[PKID_PARM].Direction = ParameterDirection.Output;
sqlParams[EMAIL_PARM].SourceColumn = CustomerData.EMAIL_FIELD;
它运用了sqlparameter对象的SourceColumn属性,该属性用于指定sqlparameter对象的值与dataset中字段的映射。它是双向,即是输入值,也是输出值。sqlParams[EMAIL_PARM].SourceColumn = CustomerData.EMAIL_FIELD;这一句表示把CustomerData(dataset对象)的EMAIL_FIELD字段值映射成sqlParams[EMAIL_PARM]的参数值。
4)
2. Web 编程技巧
1) 运用ado.net中的dataview对已存在结果集进行再查询
在cart类的AddItem方法中我们可看到源码:
public void AddItem(int itemId, String itemDescription, Decimal itemPrice)
{
DataTable itemTable = OrderItems;
DataView itemSource = new DataView(itemTable);
//search for item, check to see if the item has already been ordered
//通过itemid查找item,检查是否已预定该item,利用DataView的RowFilter属性,设置其过滤条件。
//接下来用DataView的count属性执行查询并返回满足条件的记录数
itemSource.RowFilter = "ItemNumber = " + itemId.ToString();
//如果已经订了该书,增加数量
if (itemSource.Count > 0)
{
DataRowView sourceRow = itemSource[0];
short count = (short)(sourceRow[OrderData.QUANTITY_FIELD]);
//maximum allowed for any specific item is 50
//每种书最多允许订50本
if (count < 50)
{
//bump the quantity by one
//订购书本的数量加一,更新orderdata对象(数量,总价)
count += 1;
sourceRow[OrderData.QUANTITY_FIELD] = count;
sourceRow[OrderData.EXTENDED_FIELD] = (Decimal)sourceRow[OrderData.PRICE_FIELD] * count;
}
}
//先前没有订该书
else
{
//It‘s a new item
//在orderdata里面新增一条记录
DataRow itemRow = itemTable.NewRow();
itemRow[OrderData.ITEM_NUMBER_FIELD] = itemId;
itemRow[OrderData.QUANTITY_FIELD] = 1;
itemRow[OrderData.DESCRIPTION_FIELD] = itemDescription;
itemRow[OrderData.PRICE_FIELD] = itemPrice;
itemRow[OrderData.EXTENDED_FIELD] = itemPrice;
//Add it to the table
itemTable.Rows.Add(itemRow);
}
}
3.系统配置技巧
4. 其它
1) 对象的序列化
在cart类中,我们可以看到该类实现了Iserializable接口,首先我们了解一下什么是序列化,我以前也没有接触过,所以上网查了一下,在微软的网站中找到了这篇文章---.NET 中的对象序列化(原文网址:http://www.microsoft.com/china/msdn/ ... t/html/objserializ.asp)。为什么要使用序列化?最重要的两个原因是:将对象的状态保存在存储媒体中以便可以在以后重新创建出完全相同的副本;按值将对象从一个应用程序域发送至另一个应用程序域。例如,序列化可用于在 ASP.NET 中保存会话状态,以及将对象复制到 Windows 窗体的剪贴板中。它还可用于按值将对象从一个应用程序域远程传递至另一个应用程序域。
序列化是指将对象实例的状态存储到存储媒体(例如说硬盘)的过程。在此过程中,先将对象的公共字段和私有字段以及类的名称(包括类所在的程序集)转换为字节流,然后再把字节流写入数据流。在随后对对象进行反序列化时,将创建出与原对象完全相同的副本。
要实现 ISerializable,需要实现 GetObjectData 方法以及一个特殊的构造函数,在反序列化对象时要用到此构造函数。我们看看在眼里cart类的持殊的构造函数:
private Cart(SerializationInfo info, StreamingContext context)
{
try
{
cartOrderData = (Common.Data.OrderData)info.GetValue(KEY_ORDERDATA, typeof(Common.Data.OrderData));
}
catch
{
// Leave cartOrderData null if it hasn‘t been serialized
}
}
这个构造函数在反序列化的时候被调用,用于从磁盘中获得OrderData对象。
再看看GetObjectData 方法的代码:
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
if (cartOrderData != null)
{
info.AddValue(KEY_ORDERDATA, cartOrderData);
}
}
该方法把cartOrderData对象序列化,也就相同于把它存到磁盘了。在反序列化,也就是调用上面的那个构造函数,再把它从磁盘中取出来。