一、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对象序列化,也就相同于把它存到磁盘了。在反序列化,也就是调用上面的那个构造函数,再把它从磁盘中取出来。
Duwamish作为Vs.NET自带的例子,一定代表着MS想向开发人员传递的思想和开发技巧,记得以前学习PB时没有查看PB的例子程序,等走过很多弯路再看到PB的例程时发现,原来好的冥思苦想的方法、技巧,就在自已机器上安静的躺着呢!
一直想较仔细一点的学习研究一下Duwamish,但总是没有机会,现在系统上线,总算轻松一点而有点时间来和她来个亲密接触了:)
Duwamish解决方案总共有6个项目,结构上分为5层:业务外观层(BusinessFacade)、业务规则层(BusinessRules)、业务实体层(Common)、数据访问层(DataAccess)、业务展示层(Web).另外一项目为SystemFrameWork,顾名思意,主要是用来进行整个系统构架的一些配置、跟踪、日志等.
Common项目
1.让我们来看一看Duwamish7的数据结构,图1
2.对数据库中Book,Category,Customer,Order四个最主要的对象进行了业务实体封装,四个类均继承自DataSet,但类加入两个属性:??? [System.ComponentModel.DesignerCategory("Code")]、[SerializableAttribute],前一个属性的意思是指定当前类设计器属于某一类别。后一个属性标记当前类为可序列化类,使之可远程调用。
类中对数据库中表和表中字段进行了映射,部份类中还定义了相关的错误信息如
public const String EMAIL_FIELD_NOT_UNIQUE???? = "Email Not Unique";
3.在默认的构造函数中调用BuildDataTables()方法创建表结构并添加到DataSet中。BookData、CategoryData、CustomerData类中均只保存一个表,而OrderData类中保存有与订单相关的6个表信息。
DataSet中的表与数据库中物理表有的是一一对映,有的是一个物理表对应DataSet中多个表,而有的多个物理表对应Dataset中一个表.
4.Common项目中还有一个DuwamishConfiguration类,用来获取Web.Config文件中对Duwamish配置项,继承自IConfigurationSectionHandler接口。该接口定义如下协定:所有配置节处理程序必须在实现后才能参与配置设置的解析。该接口只有一个Create()方法,DuwamishConfiguration类中实现Create()方法,通过NameValueSessionHander实例baseHandler.Create()方法填充NameValueCollection setting,再调用
ApplicationConfiguration.ReadSetting()方法返回指定的某个配置信息。
疑问:
1.4个业务实体类均有一个支持序列化的构造函数如BookData类的
private BookData(SerializationInfo info, StreamingContext context) : base(info, context) {}
不是很明白,此4个类序列化没有特别的的引用对象,应该只要进行“基本序列化”即简单的在类前加Serializable 属性对它进行标记即可,为什么还要定义一个支持序列化的构造函数呢?是不是因为继承DataSet的原因??
2.系统什么时候,怎么样调用DuwamishConfiguration类的Create()方法取得Web.Config文件中的相关配置信息?
MSDN帮助:
1.System.Configuration 命名空间
ms-help://MS.MSDNQTR.2003FEB.2052/cpref/html/frlrfsystemconfiguration.htm
SystemFrameWork项目
SystemFrameWork项目是一个能直接移植到别的应该程序的项目,可以不修改或很少的修改代码而直接使用。
项目主要为3个类。
1.ApplicationConfiguration类
此类为应用级的配置类,实现IConfigurationSectionHandler接口。与前面讲的DuwamishConfiguration类实现方法类同。主要方法为Create(),OnApplicationStart(),ReadSetting()。
如果要使用此类,需于Global.asax的Application_OnStart事件中调用 ApplicationConfiguration.OnApplicationStart(Context);
public static void OnApplicationStart(String myAppPath)
{
appRoot = myAppPath;
System.Configuration.ConfigurationSettings.GetConfig("ApplicationConfiguration");
System.Configuration.ConfigurationSettings.GetConfig("DuwamishConfiguration");
System.Configuration.ConfigurationSettings.GetConfig("SourceViewer");
}
此方法通用调用ConfigurationSettingis.GetConfig()方法通过Web.Config中的取得对应的解析类,调用相应的Create()方法。如果没有对应的配置解析类则可直接调用System.Configuration.NameValueSectionHandler类解析,GetConfig("SourceViewer")即是如此。
2.ApplicationAssert类
此类主要用来帮忙开发人员进行错误检查,日志记录等。主要有Check(),CheckCondition(),GenerateStackTrace()三方法和LineNumber属性。
[ConditionalAttribute("DEBUG")]应用于Check(),和GenerateStackTrace()方法,如果Debug常量被定义,此方法可以被调用。
3.ApplicationLog类
此类主要用来进行日志记录。
定义Error(1)、Warning(2)、Info(3)、Verbose(4)四个TraceLevel级别记录日志调试和跟踪信息.具体设置于Web.Config的配置节中定义。此类中所有方法均为static方法,是主要的为static void WriteLog(level,messageText)
如果写入的TraceLevel不大于配置中定义的级别,则把对应的调试、跟踪信息按定义的级别写入Windows 事件日志中和定义的跟踪文件中.
ApplicationLog类的构造函数声明为private static,在对ApplicationLog类进行第一次调用时从配置文件中取得配置信息进行初始化。
疑问:
1.在 static ApplicationLog()中运用了System.Threading.Monitor,保证多线程操作的安全性。为什么对ApplicationLog要进行锁的控制?
2.对System.Diagnostics命名空间中某些类理解不太清楚。
MSDN帮助:
1.Monitor 类
ms-help://MS.MSDNQTR.2003FEB.2052/cpref/html/frlrfsystemthreadingmonitorclasstopic.htm
2.System.Diagnostics 命名空间(提供特定的类,使您能够与系统进程、事件日志和性能计数器进行交互)
ms-help://MS.MSDNQTR.2003FEB.2052/cpref/html/frlrfsystemdiagnostics.htm
DataAccess项目
DataAccess项目中共4个类:Books类,Categories类,Customers类,Orders类.均实现了IDisposable接口。用来关闭活动的数据库连接。
这是MS提倡的一种释放非托管资源的Dispose模式。有关Dispose模式《.NET框架程序设计(修订本)》的19章“自动内存管理(垃圾收集)”有精采的论述.
类中通过调用存储过程,封装了对4个业务对象的Select,Insert,Update操作。
Select操作通过SqlDataAdapter的Fill方法填充对应的继承自DataSet的业务实体类,返回给调用者。Books类和Categories类只有Select操作.
Insert和Update操作通过一个private 函数返回InsertCommand或UpdateCommand对象。再于对外的的Public函数调用Private内部函数完成相应操作。Customers类中实现Insert,Update,Select操作,Orders类只有Insert操作。
在Customers类的LoadCustomerByEmail()方法中有对ApplicationAssert类的CheckCondition()方法调用
ApplicationAssert.CheckCondition(data.Tables[CustomerData.CUSTOMERS_TABLE].Rows.Count <= 1, "Integrity Failure: non-unique e-mail address", ApplicationAssert.LineNumber);
疑问:
1.数据访问层感觉实现了业务逻辑层的内容,平时如LoadCustomerByEmail()等方法都是写在业务逻辑层的。
2.是否有必要抽象出一个完全对数据库操作的基类,实现对数据库底层的操作?而不是在每个类如:Books,Customers中均一次次重写如SqlCommand对象,Dispose模式?
BusinessRules项目
1.Customer类
长见识了!在《Duwamish7学习笔记(三)》中还在想业务逻辑层写些什么东西。
校验,还是校验!每一行,每一列,包括类型和长度,想想自已在项目的校验,真的有点汗颜!
//----------------------------------------------------------------
// Function Validate:
// Validates and customer
// Returns:
// true if validation is successful
// false if invalid fields exist
// Parameters:
// [in] customerRow: CustomerData to be validated
// [out] customerRow: Returns customer data. If there are fields
// that contain errors they are individually marked.
//----------------------------------------------------------------
private bool Validate(DataRow customerRow)
{
bool isValid;
customerRow.ClearErrors();
isValid = IsValidEmail(customerRow);
isValid &= IsValidField(customerRow, CustomerData.NAME_FIELD, 40);
isValid &= IsValidField(customerRow, CustomerData.ADDRESS_FIELD, 255);
isValid &= IsValidField(customerRow, CustomerData.COUNTRY_FIELD, 40);
isValid &= IsValidField(customerRow, CustomerData.PHONE_FIELD, 30);
if ( !isValid )
{
customerRow.RowError = CustomerData.INVALID_FIELDS;
}
return isValid;
}
全部校验通过之后才调用业务访问层的相应方法进行操作,用using,使创建的Customers使用后能马上释放。
using (Customers customersAccess = new Customers())
{
result = customersAccess.UpdateCustomer(customer);
}
下面记录一点小知识
设置行错误:row.RowError
设置列错误:row.SetColumnError()
清除行错误:row.ClearErrors()
邮件校验的正则表达式:REGEXP_ISVALIDEMAIL = @"^\w+((-\w+)|(\.\w+))*\@\w+((\.
2.Order类
看完Order类觉得这各层的分工非常好,这层才是真正的业务如CalculateTax(),CalculateShipping(),InsertOrder().??? 业务逻辑层在封装业务操作方法时,进行了非常严格的校验,使系统有非常强健的健壮性。叹为观止!
疑问:Duwamish只是MS自带的一个小例子,在实际的大的项目中有非常多的对象,非常且更复杂的业务逻辑,我们的代码还能写得于此之好吗?
BusinessFacade项目
业务外观层封装了3个类CustomerSystem,OrderSystem,ProductSystem,类中直接调用数据访问层和业务逻辑层类的方法与属性提供对Web层的接口。有意思是的每个类结尾均以System命名。客户系统?订单系统?产品系统?
业务外观层中3个类均继承自MarshalByRefObject类,来支持远程处理。MSDN中关于MarshalByRefObject类的解释为:
“允许在支持远程处理的应用程序中跨应用程序域边界访问对象。”
“应用程序域是一个操作系统进程中一个或多个应用程序所驻留的分区。同一应用程序域中的对象直接通讯。不同应用程序域中 的对象的通讯方式有两种:一种是跨应用程序域边界传输对象副本,一种是使用代理交换消息。”
“MarshalByRefObject 是通过使用代理交换消息来跨应用程序域边界进行通讯的对象的基类。不是从 MarshalByRefObject 继承 的对象根据值隐式封送。当远程应用程序引用根据值封送的对象时,将跨应用程序域边界传递该对象的副本。”
“MarshalByRefObject 对象在本地应用程序域的边界内可直接访问。远程应用程序域中的应用程序首次访问 MarshalByRefObject 时, 会向该远程应用程序传递代理。对该代理后面的调用将封送回驻留在本地应用程序域中的对象。”
“当跨应用程序域边界使用类型时,类型必须是从 MarshalByRefObject 继承的,而且由于对象的成员在创建它们的应用程序域之外 无法使用,所以不得复制对象的状态。”
CustomerSystem类中有对密码通过.NET内置的加密支持进行密码保护的代码,可以很方便的用到我们平时的项目之中。
疑问:
1.类结尾命名为System,怎么解释?
2.类均标记为从MarshalByRefObject类继承,来支持应用程序中跨应用程序域边界的对象访问,系统在什么情况下将会进行“跨应用程序域边界访问对象”?
3.Common项目中的所有类均标记为[SerializableAttribute],标记为[SerializableAttribute]和继承自MarshalByRefObject类均可用来支持远程调用,两者有何区别?
4.感觉抽象出业务外观层加大了系统的复杂程度,加大了编码量。
Web项目
Web项目为Duwamish系统的业务展示层,为客户提供对应用程序的访问界面。
1.PageBase.cs为每个页面的基类,主要是获取页面的URL信息,从Session中获取或设置Customer和Cart信息,另外重写了Web.UI.Page的OnError()事件。捕获页面错误,调用ApplicationLog.WriteError()方法写入日志。
2.ModualBase.cs为系统中每个用户控件的基类,本身继承自UserControl,功能基本同PageBase.cs,用到了Session保存Customer与Cart信息.
3.Cart.cs(购物车)类中封装了所有对购物车的方法与属性。Cart类实现了ISerializable接口对OrderData对象进行序列化与反序列化。
4.Default.aspx页面没有具体很多代码,只是控件的组合。Default.aspx页面的EnableSessionState=true
5.BannerModule.ascx中所有控件的EnableViewState均设置为False,其实也可以直接在控件上设置EnableViewState=false,用户控件没有
EnableSessionState属性.BannerModule数据几乎很少改动,所以启用了缓存,在控件顶部加入代码:
“<%@ OutputCache Duration="3600" VaryByParam="none" VaryByControl="BannerModule.PathPrefix" %>”
Duration表示缓存时间为60分钟。VaryByParam="none"表示不据参数值而改变输出缓存,VaryByControl表示据每个请求的PathPrefix来
进行输出缓存。注意 :如果在指令中使用了 VaryByControl 属性,则不需要包括 VaryByParam 属性。
6.SearchModule.ascx用来进行书籍查询的模块,代码很简单,重定位的那段代码觉得写得很美观:)
Response.Redirect((new StringBuilder(PageBase.UrlBase))
.Append("/searchresults.aspx?type=")
.Append(index)
.Append("&fullType=")
.Append(Server.HtmlEncode(SearchDropDownList.Items[index].Text))
.Append("&text=")
.Append(Server.UrlEncode(searchText)).ToString(), false);
就这么几个字符串连接,都用了一个SringBuilder,还有Server.HtmlEncode(),Server.UrlEncode()方法的调用,减少不必要的麻烦。
7.SearchResult.aspx:查询结果显示页面。因为查询页面不要用到Customer和Order信息,所以页面的EnableSessionState设置为false,
EnableViewState也设置为flase.
在Page_Load()事件中取得传入的参数,生成查询结果.有段代码如下:
if (IsPostBack)
{
//
// Avoid a refresh problem if the user has left the search
// string empty by rerunning last search.
//
if ( ((TextBox)ModuleSearch.FindControl("SearchTextBox")).Text.Trim() != String.Empty )
{
return;
}
}
//Other Code
可不可以直接把Other Code放入if(!IsPostBack){}中来“Avoid a refresh problem”?
8. CategoriesModule.ascx:分类信息展示模块,因为Categories信息很少变化,把数据库中取得的分类信息保存到Cache中,设置
失效时间为60分钟之后。
9.AccountModule.ascx:创建或编辑用户账户,创建或编辑的客户保存在Session中,方便快速的获取和更新.
创建新的客户信息中密码的保存经过了层层加密。
a.用户的密码与确认密码均一致
b.通过sha1.ComuteHash计算输入密码的哈希值
c.得到的哈希值再与利用RNGCryptoServiceProvider生成长度为4的加密随机数合并生成新的字节数组
d.新的字节数组再计算新的哈希值并
e.再次加入原加密随机数,从而得到保存于数据库的加密密码。
登陆时
a.取得用户电子邮箱哈希值
b.取得用户保存于数据库中加密密码
c.取得原后4位随机加密值
d.像加密时一般,重新与当前用户输入电子邮箱哈希值进行哈希计算
e.得到的哈希值与保存于数据库中的加密密码一一对比,得到结果
哈希备注:哈希函数是现代密码系统的基础。这些函数将任意长度的二进制字符串映射为固定长度的小二进制字符串(称为哈希值)。
加密哈希函数有这样一个属性:在计算时不可能将两个不同的输入通过哈希算法取为相同的值。哈希函数通常用于数字签名和保持数据完整性。
哈希值用作表示大量数据的固定大小的唯一值。如果相应的数据也匹配,则两个数据集的哈希应该匹配。
数据的少量更改会在哈希值中产生不可预知的大量更改。
10.viewSourceModule.ascx:查看源代码的用户控件。与之相关的还有ViewSource.aspx页面和不包含于项目之中的
Docs文件夹和每个页面或控件对应的扩展名为.src的文件,来实现程序运行时查看本身的实现代码。
ViewSourceModule用户控件也启用了缓存声明如下:
“<%@ OutputCache Duration="3600" VaryByParam="none" VaryByControl="BannerModule.PathPrefix" %>”
MSDN帮助:
1.页面缓存和用户控件缓存
ms-help://MS.MSDNQTR.2003FEB.2052/cpguide/html/cpconcachingmultipleversionsofpageorcontroloutput.htm
ms-help://MS.MSDNQTR.2003FEB.2052/cpguide/html/cpconcachingmultipleversionsofusercontroloutput.htm
2.PageBase基类和ModuleBase基类均用到了Session,为什么不用Cache?两种有何区别和各自的优势?
疑问:1.查询重定向时用到Server.HtmlEncode()和Server.UrlEncode(),这两者编码方法有何区别?此处是否可以均用UrlEncode()?
2.放到页面展示的内容均进行HtmlEncode()编码。
3.DailyPickModule.ascx中有对应用中EnablePageCache配置的获取
if (DuwamishConfiguration.EnablePageCache)
{
//Enable Page Caching...
Response.Cache.SetExpires ( DateTime.Now.AddSeconds(DuwamishConfiguration.PageCacheExpiresInSeconds));
Response.Cache.SetCacheability(HttpCacheability.Public);
}
不明白,在BannerModule.ascx,CategoriesModule.ascx有对Cache的操作,在这两个地方为什么不需要进行判断?
另外,在此设置的Enable或Disable页面Cache信息对BannerModule和CategoriesModule中的Cache操作与设置有没有影响??
总结:
终于把Duwamish六个项目均较仔细的学习了一遍,感觉还是收获颇多!:)看大师出手的代码,真如品一杯美味香浓的咖啡,那种感觉真的好极了,让人赞不绝口。
1.代码写得非常简练、工整。
2.代码注释写得非常清楚、到位。
3.系统结构非常好,校验、控制写得非常的严格,使系统具有非常好的健壮性、安全性、可扩展性。
4.系统中异常处理控制得很好,不像平常我们的项目中,try,catch,散乱的随处可见。
5.系统充份利用Web.Config,进行配置,使系统可很方便、灵活的配置更改,而不用改动代码。
6.系统对View、Session和Cache的充份利用,使系统也有着很好的性能。
7.系统充份利用用户控件,继承等页向对象技术,使代码有着较好的可重用性。
虽然因为自已水平有限,还没足够深入,还没有完全理解,虽然花了好几天的时间,但是我还是想说一个字--值!
BTW:其实这是我第二次看Duwamisn7,大概是一年前就看过知道这个MS经典的例子,但记得当时看得不是太懂,当时就想先且放下,等自已有一定的DotNET基础后再来学习,实践过几个项目的今天再来看此例子觉得好理解多了:)
写完学习笔记,再看MSDN中有关Duwamish7的资料,又郁焖了,还是有太多的东西不太理解!如远程处理,分布式部署等等。如是我又对自已说:先放下吧,等再过一年,再来学习,再来研究,一定会有新的领悟,新的理解!学习就是这样,它是个渐进的过程,它不可能一蹲而就。当然如果不是时隔一年,而是半年,那就是自已飞速的进步了:)
(一).MSDN对Duwamish7的说明
Duwamish 7.0 是由 MSDN 开发的通用 Duwamish 系列应用程序的功能性端口(完全使用 .NET 技术)。尽管示例本身是围绕虚拟网上书店建立的,但本示例主要关注的方面却是性能,与从 Windows DNA 到 .NET 开发人员平台的移植技术相关的问题,设计模式,以及分布式计算环境中的现实部署方案。从功能上,它是完整的模式实现,但没有充分实现具体的履行过程(即信用卡、帐户减值、检查存货和交货)。
(二).Duwamsih7中相关技术策略
1..NET 远程处理
a.Duwamish7在信道(http,tcp)与格式化(binary,soap)组合中选择http/soap组合,这是性能和安全性方面的一个折中方案。
b.Duwamsih7支持分布式布署,允许把业务外观、业务规则层数据访问层与Web层分开布署,因此Web层要远程调用业务外观层进行处理。通过应用程序服务器上的 Web.Config 文件,和Web 服务器上的 remotingclient.cfg 文件,当应用程序启动并执行 Global.asax 的 Application_OnStart 函数时,Application_OnStart 函数调用 System.Runtime.Remoting.RemotingConfiguration.Configure 方法,以根据远程处理客户端配置文件进行远程处理配置.
Microsoft .NET 远程处理:技术概述
ms-help://MS.MSDNQTR.2003FEB.2052/dntaloc/html/hawkremoting.htm
介绍 Microsoft .NET 远程处理框架
ms-help://MS.MSDNQTR.2003FEB.2052/dntaloc/html/remoting.htm
2.数据访问策略
a.将处理转移到数据而不是将数据转移到处理。(存储过程实现所有数据处理)
b.在一个方法调用中将所有数据传递回客户端。 (FillxxxxData())
c.将数据库资源保留最短的时间。
3.错误处理
a.返回代码:在最基本的级别上 Duwamish 7.0 使用返回代码报告状态情况。
b.先决条件:Duwamish 7.0 在公共方法中测试先决条件。
c.后置条件:Duwamish 7.0 检查从函数的退出是否适当。Duwamish7.ApplicationAssert.CheckCondition 方法生成调用堆栈、记录错误信息并引发应用程序异常。
d.结构化异常处理:
e.Duwamish 7.0 使用 C# 中的 try-catch-finally 语句:
处理函数无法履行其职能的情况。
捕捉预期的错误条件。
确保发生预料外异常情况后的清理。
4.安全策略
Duwamish的安全策略MS承认:不要在生产环境中使用 Duwamish 7.0 示例。它是一个示例,因此在设计上并不十分安全。
如果原封不动地使用该示例,则可能会给您的产品环境带来不必要的安全风险。
推荐的安全策略:
a.用于限制程序集使用的代码访问安全性
b.分布式方案
c.基于窗体的安全性
d.密码凭据保护
e.使用基于角色的安全性向用户授权
f.Windows 身份验证和 SQL Server
5.用户服务
Duwamsih7中充分利用Web 窗体和服务器控件、缓存、会话状态、视图状态、安全性、XML Web services等为用户提供优质、安全的用户服务。
浙公网安备 33010602011771号