摘要
最初研究 .NET Pet Shop 的目的是用 Microsoft .NET 实现 Sun 主要的 J2EE 蓝图应用程序 Sun Java Pet Store 同样的应用程序功能。 根据用 .NET 实现的 Sun J2EE 最佳实践示例应用程序,各方面的客户可以直接地对 Microsoft 的 .NET 技术与基于 J2EE 的应用程序服务器进行比较,同时了解构建基于Web的应用程序中用到的各种建议的设计模式之间的异同。 .NET Pet Shop 应用程序现在已经是第三版了,旨在显示构建企业级 n 层应用程序(可能需要支持多种数据库平台和部署模型)的 .NET 最佳实践。 根据社区对 .Net Pet Shop 2.0 的反馈,.NET Pet Shop 3.0 遵照 MSDN 上发布的 Microsoft《说明性体系结构指导》进行了重新设计。 第三版还完全符合了 Middleware 公司的应用程序服务器基准测试规范,将作为 Microsoft 参加今年春天即将进行的 Middleware Application Server Benchmark 的产品: 这是Middleware 公司举办的第二轮测试活动,旨在比较 .NET 和 J2EE 平台在构建和承载企业级 Web 应用程序方面的可伸缩性。
返回页首
Java Pet Store 是 什么?
Java Pet Store 是按 Sun 公司维护的 J2EE 蓝图开发的分布式应用程序的一个参考实现。 示例应用程序最初的开发目的是帮助开发人员和架构师理解如何使用和利用 J2EE 技术,以及各个 J2EE 平台组件是如何配合的。 Java Pet Store 演示软件包括构建应用程序所需的Enterprise Java Beans (EJB) 体系结构、 Java Server Pages (JSP) 技术、标记库和 servlet 的完整的源代码及文档。 此外, Java Pet Store 蓝图应用程序还通过具体示例说明了一些模型和设计模式。
完整的 Java Pet Store 包括三个示例应用程序:
• Java Pet Store: J2EE 蓝图主应用程序。
• Java Pet Store 管理器: Java Pet Store 的管理器模块
• Blueprints Mailer: 在小一些的包中给出一些 J2EE 蓝图设计指南的一个小应用程序。
Java Pet Store 的最初版本旨在处理以下数据库: Oracle、Sybase 和Cloudscape 。 IBM 已经开发了一个 DB2 版本的应用程序。 该应用程序可以从 Java 2 Platform Enterprise Edition Blueprints 公开获得。 主应用程序 Java Pet Store 是一个电子商务应用程序,可以通过它在线购买宠物。 启动应用程序后,可以浏览和搜索各种类型的宠物,从狗到爬行动物。
使用 Java Pet Store 的典型会话方案如下:
主页 — 这是用户第一次启动应用程序时加载的主页。
类别查看 — 有五大类: 鱼、狗、爬行动物、猫和鸟。 每一类都有几个相关的产品。 如果选择鱼作为类别,可以看到天使鱼等等内容。
产品 — 如果现在选择一个产品,应用程序将显示产品的所有类型。 通常产品类型是雄或者雌。
产品详情 — 每种产品类型(分别用不同项目表示)有详细的视图显示产品说明、产品图像、价格和库存数量。
购物车 — 用户可以通过它操作购物车(添加、删除和更新行项目)。
结帐 — 结帐页面以只读视图显示购物车。
登录重定向 — 当用户选择结帐页面上的“Continue”时,如果还没有登录,将重定向到登录页面。
登录验证 — 通过站点的身份验证以后,用户被重定向到信用卡和记帐地址表单。
定单确认 — 显示记帐地址和送货地址。
定单提交 — 这是定单处理流程的最后一步。 定单现在将提交到数据库。
Microsoft .NET Pet Shop
.NET Pet Shop 的目标是把注意力仅仅放在 Java Pet Store 上(管理和Mailer 组件没有在 .NET 中实现)。 除了重现 Java Pet Store 应用程序的功能之外,还增加了两项目标:
• 比较 .NET 和 J2EE 通过最佳实践实现的真实应用程序中代码和代码大小上的异同。
• 提供用 .NET 和 J2EE 实现的典型的设计良好的应用程序能够支持多少用户的数据。
.NET Pet Shop 的整体逻辑体系结构如图 3 所示,设计的中心是在表示层使用 ASP.NET Web 窗体,与逻辑中间层中的 C# 业务组件通信。 业务组件继而通过 ADO.NET 和 SQL Server 名为数据访问应用块 (DAAB) (可以从此链接了解更多 DAAB 信息并下载完整的 DAAB 源代码)的帮助器类访问后端数据库。 数据访问功能完全抽象到数据访问层 (DAL) 中,与业务逻辑层 (BLL) 相分离。 .NET Pet Shop 3.0 中的新颖之处在于,我们为 Oracle 9i 和 SQL Server 2000 数据库都引入了 DAL 层。 相应 DAL 层的类加载将根据 Web.Config 中的应用程序配置设置在运行时动态生成。注意 .NET Pet Shop 3.0 使用了两个后端数据库,定单处理中要涉及跨两个数据库的分布式事务。 使用简单的 Web.Config 应用程序设置,用户可以对 .Net Pet Shop 进行部署,使用一个或者多个后端数据库,还可以自由地将 SQL Server 和 Oracle 后端数据库与由 .NET 服务的组件通过 COM+ 企业服务处理的分布式事务混合。
图 4 说明了 Microsoft .NET Pet Shop 物理上是怎样部署的。 这里使用网络负载平衡 (NLB) 或者可能是硬件实现的负载平衡技术将入站的网络通信量分到了两台应用程序服务器上。 在网络请求达到群集中的一台机器时,针对该请求的所有工作都会在这台特定机器上进行。 业务逻辑和数据访问组件将以程序集的形式安装在两台服务器上,它们本质上是完全相同的。 如果负载平衡软件配置为使用“Sticky IP”,则每台服务器都有自己的会话-状态存储,因为要保证第二个请求返回到实现第一个请求的那台服务器。 如果解决方案所需的容错要求更高,两台应用程序服务器可以共享一个公共会话-状态存储比如 SQL Server 或者一台专用的会话服务器(图中没有显示)。 会话-状态存储的类型和位置由每个站点‘web.config’文件里‘system.web’元素‘sessionState’子节点中的值决定。
业务需求
作为 Pet Shop 3 体系结构文档的一部分,我们给出了 .NET Pet Shop 的业务需求,这样开发人员和客户就可以理解我们在做应用程序的设计决策时进行的一些选择。
Pet Shop 应用程序的功能性需求是什么?
• 应用程序应该使客户能够按类和通过关键字搜索浏览公司目录。
• 应用程序应该为客户提供一种通过一个购物车模型就能购买多个商品项的机制。
• 应用程序应该提供简单的安全模型,这样客户必须先进行登录,才允许购买购物车的内容。
• 应用程序旨在支持高容量的企业级电子商务解决方案;因此应用程序应该展示以下方面:
• 高性能,通过所支持用户数和用户响应时间进行衡量
• 通过增加更多处理器来扩展的能力
• 通过增加更多机器组成群集的分布式扩展能力
• 在大型企业级系统中,应用程序可能需要访问多个数据库,因此应用程序应该支持分布式事务。
• 应用程序应该考虑灵活的部署策略。 默认时应用程序的设计方案是要部署到两台机器上,一台是应用程序服务器,一台是数据库服务器,但是应该能够扩展在其他部署模型下工作。 应用程序应该支持多个数据库供应商。 这里我们选择了 Microsoft SQL Server 和Oracle。
• 应用程序应该容易维护,这是通过应用程序中的代码行数来衡量的。
返回页首
应用程序数据模型
.NET Pet Shop 中使用的数据库架构是直接从 Java Pet Store 移植而来的。 Java Pet Store 支持几种数据库供应商格式,因此我们选取了 Sybase 应用程序的架构,并在一个 Microsoft SQL Server 2000 实例中创建。 这不需要改变 Sybase 版本的架构。 而创建 Oracle 版本的 .NET Pet Shop 时,我们直接采用了 Java Pet Store 数据库原来的 Oracle 实现。
数据库 有如下整体表结构,参见表 1:
表 1. Pet Shop 中的数据库表
表名 用途
Account
代表基本客户信息
BannerData
存储广告条信息
Category
目录类别( Fish, Dogs, Cats 等)
Inventory
产品库存状态
Item
各个产品的细节
LineItem
定单细节
Orders
客户下的定单。 定单包括一个或多个行项目
OrderStatus
定单状态
Product
目录产品,每个产品可有一或多类型(项目)。 通常类型可能是雄或雌。
Profile
客户的用户配置情况
Signon
客户登录表
Supplier
有关供应商信息
在 .NET Pet Shop 版本 2 中,应用程序改为要创建一个方案,其中完成定单处理必须使用分布式事务。 为了适应分布式事务方案,Orders、OrderStatus 和 LineItem 表都分到不同的可能安装在不同机器上的数据库实例。 我们在 .NET Pet Shop 的第三版中保持了这个分布式设计模式。
Pet Shop 表的设计可以做什么更改?
应用程序中使用的架构可以做一些更改;然而,这些更改并不是为了与 Java Pet Store 参考实现提供的架构一致。 这些更改已列于表 2 中:
表 2. Pet Shop架构中可能的改进
更改 原因
表中不存储 HTML
因为可能要使用不同客户端应用程序类型,数据库仅存储图像文件名而不是图像标记,部署客户端类型可以更灵活。
对客户密码使用单向加密算法
帮助使应用程序更安全,因为即使系统任何部分的安全受到威胁,读取密码仍然很困难。 这是要求设施必须重置密码为新值。
加密信用卡信息
在系统安全受到威胁时防治对信用卡信息的访问。 做出的其他更改还有将表设为只能允许通过一个存储过程进行写入访问;因此黑客访问数据库将不得不需要另一套凭据来读取数据。
返回页首
.NET Pet Shop 2.0 体系结构
.NET Pet Shop 2.0 被设计成部署在物理上两层的部署环境中,并且在应用程序的一些部分的实现中利用了这一事实。 应用程序由以下部分构成:一个用 ASP.NET Web 窗体 (用“代码隐藏”将应用程序 HTML 和用户接口代码分离)创建的 Web 层。 一个包含控制应用程序逻辑的业务组件(通过自定义版本的 Microsoft 数据库访问应用程序块 (DAAB) 与 SQL Server 数据库通信)的中间层。 为了支持分布式事务,一些中间层业务组件是用企业服务实现的。 对于 Microsoft .NET,这是一种支持分布式事务的建议方式。 然而,并非所有类都要扩展 ServicedComponent 类,因为将所有类都实现为企业服务的组件是有性能开销的。 发布第一次 Middleware 应用程序服务器基准测试中使用的 .NET Pet Shop 2.0 时,我们收到了许多反馈,认为体系结构应该优化,以更适应于大规模的企业。 反馈比较集中的方面包括:
• 创建完全抽象的数据层,无需在中间层导入数据特定的类。
• 为 Oracle 实现数据访问层,可以透明地使用与 SQL Server 版本一样的业务层和 UI 层。
• 将 Web 会话状态从业务逻辑层和数据层中完全提取出来,这样应用程序的后端组件可以在物理上分布到 Web 服务器之外的其他计算机上,或者从其他类型的客户端比如基于 Windows的客户端和基于 Web– 的客户端重用。
• 将应用程序模块整个分为多个命名空间和物理程序集。
• 其他各方面的反馈。
返回页首
.NET Pet Shop 3.0 体系结构
应用领域
表 3. Pet Shop 解决方案中的应用领域
范围 用途 .NET 实现
用户接口组件
捕获来自用户的数据输入,显示后端系统返回的数据。 它们还处理简单的定位。 参见用户接口组件。
ASP.NET Web 窗体,用户控件和服务器控件。 这些构造能够清晰地分离设计者的 HTML 和 UI 代码比如按钮的事件处理程序。
用户接口处理
用后端业务对象控制用户定位和处理流程。 还要处理用户会话数据的管理。 参考用户处理组件。
这些是用 C# 类实现的。 会话状态管理由 ASP.NET 处理。
业务组件
实现应用程序业务逻辑的组件
这些是用 C# 类实现的
业务实体
在应用程序各层之间传递数据的瘦数据类。 参见业务实体组件。
这些是用 C# 类实现的,每个字段都以属性的形式公开。 每个类都标记为“serializable”,启用进程间传输。
数据访问层组件
处理与后端数据存储区的交互,包括数据库、消息处理系统等。
这些组件处理与后端数据存储区的交互,包括数据库、消息处理系统等,是用四个 C# 项目实现的:
• 一组接口类,要公开的每个数据访问方法都有。
• SQL Server 接口的实现。
• Oracle 9i 接口的实现。
• 一组加载正确实现的工厂类, SQL Server 或 Oracle。
Microsoft Visual Studio .NET 解决方案
图 9 显示了 Microsoft Visual Studio .NET 解决方案对 .NET Pet Shop 应用程序的布局。 应用程序的每个元素或层都有自己的项目,这样解决方案可以管理和清晰地定义应用程序中使用的新类应该放在哪里,旧类又可以在哪里找到。
表 4 中列出了每个项目的目的:
表 4. Pet Shop 解决方案中的 Visual Studio 项目
项目 用途
BLL
业务逻辑组件存放之处
ConfigTool
用来加密连接字符串和创建事件日志源的管理应用程序
DALFactory
用来确定加载哪一个数据库访问程序集的类
IDAL
每个 DAL 实现都要实现的一组接口
Model
瘦数据类或业务实体
OracleDAL
Oracle 特定的 Pet Shop DAL 实现,使用了 IDAL 接口
Post-Build
运行编译后操作的项目,比如将程序集添加到 GAC 或 COM+
Pre-Build
将程序集从 GAC 删除或从 COM+ 注销程序集的项目
SQLServerDAL
Microsoft SQL Server 特定的 Pet Shop DAL 实现,使用了 IDAL 接口
Utility
一组帮助器类,包括 DPAPI 的包装
Web
Web 页和控件
Solution Items
用来构建应用程序的杂项,比如用来签署应用程序程序集的 Pet Shop.snk 密钥文件
数据库可移植性
这一版本 Microsoft .NET Pet Shop 的关键需求之一是提供支持 Oracle 和 SQL Server 数据库的应用程序实现。 在设计应用程序的数据库访问机制时,我们可以选择应该使用哪一个数据库提供程序;可以使用通用的 OLE-DB 托管提供程序或者数据库特定的优化了性能的 .NET 托管提供程序,比如 .NET 框架1.1 中提供的 SQL Server 和 Oracle 托管提供程序。 应用程序的关键需求之一是创建一个高性能的解决方案,因此我们选择用数据库本身的 .NET 托管提供程序构建应用程序。 关于托管提供程序和通用 OLE-DB 提供程序之间的性能差异分析,读者可以参考 Using .NET Framework Data Provider for Oracle to Improve .NET Application Performance,该文档说明了厂商特定的提供程序能够比等价的 OLE-DB 提供程序性能好两到三倍。 在选择数据库特定的访问类时进行的考虑是,我们需要为每个要支持的数据库平台写一个单独的数据访问层,因此应用程序将包含更多代码。 虽然两个数据访问层共享很多公共代码,但还是要明显地分别针对具体数据库(Oracle 或 SQL Server 2000)。
为了简化数据库访问类的使用,我们选择 GoF (译注:指 Erich Gamma 等著《设计模式》一书)概述的工厂设计模式,通过反射动态在运行时加载正确的数据访问对象。 工厂设计模式是这样实现的: 创建一个 C# 接口,其中对于数据库访问类必须公开的每个方法都要声明一个方法。 对于每一个要支持的数据库,都创建一个实现数据库特定代码的具体类,以执行接口也称“协定”中的每一项操作。 为了支持运行时确定加载哪一个具体类,需要创建第三个类,也就是工厂类,它从配置文件中读入一个值以确定应该使用反射加载哪一个程序集。 通过 .NET 的反射命名空间,可以加载某个特定程序集并用该程序集创建某个对象的实例。 为了使应用程序更安全,为版本控制提供更好的支持,我们可以在应用程序配置文件(也就是这里的 web.config. )中添加要加载的程序集文件的“证据”,这意味着 .NET 框架将只加载我们在编译期间签过名而且有正确版本号的程序集。 图 10 说明了业务逻辑类、工厂类和数据库访问类是如何相互操作的。 这一创建的解决方案最重要的优势是数据库访问类可以在业务逻辑类之后编译,只要数据访问类实现了 IDAL 接口。 这意味着,如果要创建应用程序的 DB2 版本,我们不需要改动业务逻辑层(或者 UI 层)。 创建 DB2 兼容版本的步骤如下:
1.创建 DB2 的数据库访问类,它应该实现 IDAL 接口。
2.将 DB2 访问类编译成一个程序集。
3.测试和部署新的数据程序集到一台运行中的服务器上。
4.更改配置文件,指向新的数据库访问类。
无需更改或重新编译业务逻辑组件。
存储过程
通常我们都建议客户使用存储过程来访问数据库中的表。 原因如下:
• 存储过程提供了封装查询的一种简洁机制。
• 修改查询可以在不改变数据访问代码的情况下进行。
• DBA 可以很容易地看到正在执行什么 SQL 语句。
• 存储过程一般更安全,对数据库访问的控制也更容易。
• 使用存储过程,可通过在存储过程中发送多个请求,避免与客户端的多次往返行程。
• 存储过程与中间层生成的 SQL 相比,通常能提供最佳性能。
• 存储过程提供了极好的封装 XML 查询和 XML 输入参数的方式。
存储过程的缺点在于,它们往往是专有的,不能跨平台移植。
然而,要想最大程度地利用在数据库软件和硬件上已经花费的投资,开发人员往往对应用程序中使用的 SQL 针对具体数据库引擎进行优化,无论 SQL 是在存储过程中还是在中间层生成的。 这一点有一个很好的例子,就是唯一编号或者标识编号的生成,因为所有数据库执行此操作时都支持自己的特殊机制,所以用来生成唯一编号的 SQL 就往往是特定于所用数据库的。 一般总是有替代方案的,但是它们的执行速度都比不上专有解决方案。
对于 .NET Pet Shop,我们有意识地没有在应用程序使用存储过程,因为这在 Middleware 基准测试中会被看作是 .NET 解决方案一种不太公平的优势。 实际上,这方面的性能差异很小,因为应用程序相对比较简单,大多数 SQL 语句的执行计划都缓存在数据库中了。 但是, Middleware 基准测试规范不允许使用存储过程,哪怕只是包装简单的 SQL 语句,因此 .NET Pet Shop 3.0 没有使用存储过程。
缓存
最有效的提高数据库驱动的应用程序性能的方式,是避免对每次请求都访问数据库。 ASP.NET 提供了各种缓存机制以提高大多数应用程序中的性能。 ASP.NET 中使用缓存的两种主要方式是输出缓存和数据缓存。
.NET 缓存选项
页面级输出缓存接收来自 ASP.NET Web 页的响应,并将整个页面存入缓存中。 页面缓存被设计成工作在 Web 层和中间层之间,缓存中间层方法的结果/数据,或在两层应用程序中缓存数据库调用结果。 第一版的 .NET Pet Shop 同时提供了一个页面级输出缓存版本和一个非缓存版本。 第三版只支持数据缓存,但是可以很容易地改为支持输出缓存。 对于 Windows Server 2003 和 IIS 6.0,有些输出缓存的页面(那些 VaryByParm="none" 而且有 Cache 'Anywhere' 指令的)还能在内核级进行缓存,Internet 客户端访问就更快了。 无论如何,任何输出缓存的页面(内核或者非内核缓存)Windows 应用程序服务器都可以在资源 (CPU) 消耗少得多的情况下,极快速地进行服务,因为实际上重新创建页面无需进行处理。
ASP.NET 输出缓存
最早的 .NET Pet Shop 应用程序,版本 1.5,还使用了一种页面级输出缓存的变种,也就是部分页面缓存或称片段缓存来缓存页面的不同区域。 例如,缓存每个页面顶部的头信息。 然而,头信息取决于正在登录的用户(因此两个不同版本的页面都要缓存)。 ASP.NET 很容易允许这种操作,使用‘OutputCache’指令中的 VaryByCustom 属性即可。 使用 VaryByCustom 需要重写 GetVaryByCustomString 方法以获取头信息的自定义缓存。
ASP.NET 数据和对象缓存
对象缓存(缓存 API)允许使用 .NET 框架在内部缓存引擎中存储方法调用或者数据库查询的结果。 由于已经深入了应用程序工作管道,数据缓存可能无法像输出缓存那样提供同样的性能提升,因为对每个请求仍然必须动态构造 HTML 页面。 但是,通过在中间层存储非易失数据,已经在完全动态的页面和减少数据库负载之间取得了很好的折衷。 例如,要在两个 Web 页中以不同方式显示同样的数据,或者在同一个应用程序不同页面中以不同方式使用已经缓存的对象或数据。
ASP.NET 缓存监视
监视 ASP.NET 缓存系统中发生了什么,有几种方式。 首要的方法是使用 Perfmon,但是还可以使用 SQL Server 跟踪检查访问数据库的时间,这根据具体情况而定。 为了在 Perfmon 中监视缓存,ASP.NET Application 性能对象下选择 Output Cache Entries 和 Cache API Entries 计数器,将它们添加到 Perform 图。 还可以在 Perfmon 中监视周转率和点击率,检查缓存中是否使用了某个项目。
表 5. .NET 缓存选项摘要
缓存类型 优点 限制
输出缓存
提供了最佳性能
整个页面输出都缓存
片段缓存
(缓存用户控件)
实现很简单。 整个页面输出都缓存
有时间失效限制
缓存 API
页面中不同用户控件可以有不同的缓存超时
缓存控件可以跨页面共享。 需要分别缓存每个用户控件
Pet Shop Middleware 基准测试缓存规则
Middleware 公司为基准测试应用程序定义了严格规则:什么能够缓存,怎样缓存。 大体而言,页面级输出缓存是禁止的,但是应用程序中的一些地方允许中间层数据缓存。 以下数据允许缓存,不需要每次请求都从数据库刷新: 类别信息,产品信息,项目信息(库存数据除外)和某些帐户信息。 对于库存数据,无论何时项目添加到购物车或者用户定位到‘ItemDetails’页面时,库存中的当前量应该反映最新值。 对于帐户信息,用户名和密码对于每一个登录企图都要进行验证,用作计帐信息的地址也应该始终从数据库刷新,以确保它在结帐处理中是最新的。 这里也就意味着,可以使用设置了给定期限的数据/对象缓存将大多数产品搜索和浏览操作的结果缓存在中间层中。
基准测试的第二条规则是不应该允许页面级输出缓存;应始终要求 Web 服务器重新为页面创建要呈现的 HTML。 这是为了测试应用程序服务器生成动态页面的能力,而不是测试服务器从缓存拖出 HTML 有多快。
Pet Shop 3.0 缓存实现
对于这一版本的应用程序,我们使用 ASPX 页面后代码中的数据缓存,来缓存中间层(业务逻辑层)请求的结果。 以下示例代码说明了如何访问 ASP.NET 缓存 API 从缓存检索信息。 该示例基于 Pet Shop 应用程序中的类别查询代码,category.aspx.cs。 在 Pet Shop 中,用户可以在五个预定义宠物类别中选择一个,查看该类别中的动物列表。 代码所做的第一件事是检查数据是否已经缓存,这是通过用类别 id 作为键查找数据缓存中的一个元素来实现的。 如果这个元素不存在,要么是因为数据还没有缓存,要么是当前缓存已经过期,将返回 null。 如果元素存在,就从缓存中拖出数据,并转换为合适的类型。 如果数据不在缓存中,调用中间层查询数据。 中间层查询的结果然后添加到缓存中,期限为从现在开始 12 小时。 或者,可以根据固定时间、对另一缓存项目的依赖,或者通过提供一个可以用来清除缓存的回调函数使缓存过期。
// Get the category from the query string
string categoryKey =
WebComponents.CleanString.InputText(Request["categoryId"],50);
// Check to see if the contents are in the Data Cache
if(Cache[categoryKey] != null){
// If the data is already cached, then used the cached copy
products.DataSource = (IList)Cache[categoryKey];
}else{
// If the data is not cached,
// then create a new products object and request the data
Product product = new Product();
IList productsByCategory = product.GetProductsByCategory(categoryKey);
// Store the results of the call in the Cache
// and set the time out to 12 hours
Cache.Add(categoryKey, productsByCategory, null, DateTime.Now.AddHours(12), Cache.NoSlidingExpiration , CacheItemPriority.High, null);
products.DataSource = productsByCategory;
}
// Bind the data to the control
products.DataBind();
返回页首
小结
.NET Pet Shop 的体系结构已经进行了改进,能够在部署选择方面提供更灵活的解决方案,应用程序能够更加容易地进行自定义,适应业务模型中的变化。 尽管有所有这些变化,Pet Shop 的性能与 2.0 版实现大致相同,说明了 Microsoft .NET 框架在 J2EE 之外提供了另一种可行的企业级解决方案。 通过 .NET Pet Shop 和 J2EE Pet Store,架构师和开发人员可以使用功能完全一样、说明了各自平台上最佳编程实践的参考应用程序,一对一地比较 .NET 和 J2EE。 即将进行的 Middleware 基准测试将测试新的 .NET Pet Shop 3.0 实现,将比较它与两个新 J2EE 实现——一个基于 CMP 2.0,另一个基于纯粹的 JSP/Servlet 体系结构(不使用 EJB)的性能。 测试将由 Middleware 公司进行,在运行着各种 J2EE 应用程序服务器的服务器端发布。 将组成一个 J2EE 专家评委会监督这一基准测试,以确保对被测试产品的公平性以及所有实现都符合规范和最佳准则。 此外,已经邀请了主要的 J2EE 应用程序服务器供应商,加上 Microsoft 来参与这一基准测试,他们将对规范进行评价,提供供测试的实现,并现场进行微调和优化,参与测试过程。 Microsoft 选择全面参与第二轮测试。 可以到这里 了解 Middleware Application Server 测试第一轮的细节,测试基于两个基准测试应用程序:早期的 .NET Pet Shop 2.0 和一个基于 BMP 的 J2EE 实现,或者下载完整的报告。
返回页首
附录 A: 从版本 2 到版本 3 的更改
项目 说明 原因
1
创建数据库访问层 (DAL)。
可以将业务逻辑从数据库访问代码中完全分离出来,因此可以无需更改业务逻辑代码,即可更换数据库供应商。
2
为所有瘦数据类(模型)创建公共的项目。
模型项目只包含保存数据的瘦类,可以用在应用程序的每一层,作为传输数据的容器。所有模型类都通过 [Serializable] 标记支持序列化,以添加对群集和任何未来物理部署中变化的增强支持。
3
在 Components 项目中删除对 System.Web 的引用。
这是一种好的设计实践,因为可以使中间层组件用于不同类型的 UI。
4
将一些自定义服务器控件改为 Web 用户控件;然而 Pager 控件仍然保留为自定义服务器控件。
标头和标题控件含有许多 HTML/UI 内容,因此如果实现为 Web 用户控件更易维护。 Pager 控件处理的是查询字符串的操作,视图状态中数据的使用,因此作为自定义服务器控件更合适。
5
为 Oracle 创建了 DAL 实现。
为了支持 Oracle 数据库,创建了特定的 Oracle DAL,可以使用 Oracle 特定的驱动程序
6
DAL 层现在实现了工厂模式 [GoF] 以加载相应的供应商特定的 DAL。
为了隐藏后端所使用的数据库,使用工厂方法将接口返回到要用到的 DAL 层。
7
模型类中的所有公共字段都转换为属性。
这样字段的存储机制就可以隐藏起来,如果要看什么函数修改了某些数据,这提供了一个很好的在代码中添加断点的地方。
8
创建数据库访问层 (DAL)。
可以将业务逻辑从数据库访问代码中完全分离出来,因此可以无需更改业务逻辑代码,即可更换数据库供应商。
9
将所有静态方法改为实例方法。
根据评论反馈进行的修改
10
将 assemblyinfo.cs 中的版本号改为与部署匹配的具体版本。
在 DALFactory 对象中加载程序集时,允许指定证据。
11
在 Visual Studio .NET 解决方案中添加构建前和构建后的步骤。
这样,在项目编译后,就可以对具体程序集运行 gacutil 和 regsvcs 实用工具。
12
将订购业务组件改名,根据其执行的功能而非实现机制。
对其他开发人员而言理解组件的功能更直观。
13
创建配置工具辅助应用程序的设置。
需要以管理员组成员的身份创建应用程序事件日志源。 确保完成最简单的方式是提供一个简单的工具,可以在部署后运行。
14
将代码修改为允许帐号和产品数据库以及定单数据库使用不同的 DAL。
对用户请求的响应应该能够使用混合数据库部署模型。
15
页面中添加页面输出缓存;但是基准测试时要删除。
有用户请求说明如何重写 VaryByCustom 函数使标题和标头控件等页面能够在 default.aspx 这样的页面中缓存。
16
将处理流程控件类添加到 Web 应用程序中。
根据评论反馈做的修改,为帐号或购物车这样的 Web 区域提供单一位置来控制定位和状态管理。
关于该系统的大致介绍可以从上面的连接获得,都是中文的。
下面来分析一下PetShop3.0的用户注册部分(我今早上刚研究的,哈,趁热端出来)
PetShop3.0是业务实体和业务逻辑分开的,并且在表示层上也有逻辑处理。业务实体部分从前到后都有用到。实际上,在传递数据的时候就是传递的一个实体,而不是像我们一般用的一个变量一个变量的传,在用户注册中也是这样。
注册页面是CreateAccount.aspx,这里有一个usercontrol:AddressUI,用来收集用户的一般信息,其他的个人网站设定和用户名密码什么的都是分开来取的,通过提取AddressUI.Address来获得一个AddressInfo对象,然后用这些信息创建一个AccountInfo对象,最后调用ProcessFlow.AccountController的CreateAccount方法来完成注册。CreateAccount接收的参数自然是一个AddressInfo类型的对象,返回类型为bool。根据返回值来判断注册是否成功。实际上,它这里假定如果不成功,那就只有一种情况,就是用户名已经被注册了。
接下来的事情就是一层套一层的引用了。把业务实体AccountInfo一层的往下传,最后到达SQLServerDAL层,这里的Insert方法执行最后的操作。
PetSop.Web.ProcessFlow.AccountController :
public bool CreateAccount(AccountInfo newAccountInfo){
try {
// Creata a new business logic tier
Account account = new Account();
// Call the insert method
account.Insert(newAccountInfo);
// Store the data in session state and store the authenticated cookie
HttpContext.Current.Session[ACCOUNT_KEY] = newAccountInfo;
FormsAuthentication.SetAuthCookie(newAccountInfo.UserId, false);
//Finally forward to the welcome page
HttpContext.Current.Response.Redirect(URL_ACCOUNTCREATE, true);
}
//注意在这里捕获异常,说明用户名已存在。详细描述见下面
catch {
return false;
}
return true;
}
PetShop.BLL.Account :
public void Insert(AccountInfo account) {
// Validate input
if (account.UserId.Trim() == string.Empty)
return;
// Get an instance of the account DAL using the DALFactory
IAccount dal = PetShop.DALFactory.Account.Create();
// Call the DAL to insert the account
dal.Insert(account);
}
最后进入实际的数据操作层
PetShop.SQLServerDAL.Account :
public void Insert(AccountInfo acc) {
SqlParameter[] signOnParms = GetSignOnParameters();
SqlParameter[] accountParms = GetAccountParameters();
SqlParameter[] profileParms = GetProfileParameters();
signOnParms[0].Value = acc.UserId;
signOnParms[1].Value = acc.Password;
SetAccountParameters(accountParms, acc);
SetProfileParameters(profileParms, acc);
using (SqlConnection conn = new SqlConnection(SQLHelper.CONN_STRING_NON_DTC)) {
conn.Open();
using (SqlTransaction trans = conn.BeginTransaction()) {
try {
SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_INSERT_SIGNON, signOnParms);
SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_INSERT_ACCOUNT, accountParms);
SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_INSERT_PROFILE, profileParms);
trans.Commit();
}
//违反约束,抛出异常
catch {
trans.Rollback();
throw;
}
}
}
}
那么它是怎么判断用户名是否已经被注册了呢?原来,在保存用户名和密码的表里有一个主键约束,这样自然就插不进重复的用户名。一旦有相同的用户名进入,就会违反约束,抛出异常,当然在之前还要回滚事务,抛出的异常在表示层CreateAccount方法中被捕获,方法返回false,最后反映到页面上。
我在这里就有一个疑问,这样做不是把异常作为一种控制流程的手段了吗?
《effective java》第39条“只针对不正常的条件才使用异常”。根据这一条,万一不能注册是因为其他不可预料的原因而发生的呢?这也会返回给要注册的用户“Duplicate user ID! Please try again.”信息。而且我以前看过一篇文章,说.net的异常抛出会消耗大量的资源,建议不要把异常做为一种实现的方法。其实这里完全可以用T-SQL编程的手段来预先判断用户名是否存在,然后再采取下一步措施。
宠物展示,也就是商品展示(汗……)
涉及到这个主题的页有Category.aspx / Items.aspx / ItemDetails.aspx,分别是大类/小类/详细信息这三个。下面来一个一个的分析
要注意的是,像第一篇说的,数据的传递都是采用直接传递业务实体的方法来完成。这样是不是有很强的面向对象的味道?
Category.aspx
该页面的主体是一个用户自定义控件:SimplePager,该空间继承自Repeater,作用当然是存放宠物信息,他的数据源的获得方式使用了.net的缓存api。具体实现代码为:
if(Cache[categoryKey] != null){
// If the data is already cached, then used the cached copy
products.DataSource = (IList)Cache[categoryKey];
}else{
// If the data is not cached, then create a new products object and request the data
Product product = new Product();
IList productsByCategory = product.GetProductsByCategory(categoryKey);
// Store the results of the call in the Cache and set the time out to 12 hours
Cache.Add(categoryKey, productsByCategory, null, DateTime.Now.AddHours(12), Cache.NoSlidingExpiration , CacheItemPriority.High, null);
products.DataSource = productsByCategory;
}
// Bind the data to the control
products.DataBind();
是使用了很普遍的做法来完成的,新数据的通过PetShop.BLL.Product.GetProductsByCategory方法获得,梢后分析这个方法。SimplePager获得数据后,就像普通的Repeater一样根据模板的设定来显示数据。对SimplePager的分析同样放在后面。
用户点击具体的小类后,就进入小类的页面,通过url传递小类的编号。
Items.aspx
和上面的页面基本一样,因为都是对类别进行的操作嘛。
选择具体的宠物后,就进入详细资料展示页面了。
ItemDetails.aspx
这个页面没有使用缓存,而是直接通过业务逻辑层到数据库去取记录,然后返回到表示层。很简单,一看就懂的那种。
下面来看一下SimplePager控件。
控件的定义中重写了很多Repeater的方法,其用意主要是为了是其具有分页的功能,看一下其中的Render方法。
PetShop.Web.Controls.SimplePager : Repeater
override protected void Render(HtmlTextWriter writer) {
//Check there is some data attached
if (ItemCount == 0) {
writer.Write(emptyText);
return;
}
//Mask the query
string query = Context.Request.Url.Query.Replace(COMMA, AMP);
query = RX.Replace(query, string.Empty);
// Write out the first part of the control, the table header
writer.Write(HTML1);
// Call the inherited method
base.Render(writer);
//从这里往下开始是重写的关键部分,用来显示上下页的按钮
// Write out a table row closure
writer.Write(HTML2);
//Determin whether next and previous buttons are required
//Previous button?
if (currentPageIndex > 0)
writer.Write(string.Format(LINK_PREV, (currentPageIndex - 1) + query));
//other .....
}
另外该控件还有自定义的事件PageIndexChanged,用来对页面切换进行响应。DataSource接受实现IList接口的数据,而我们看上面大类页面在显示的时候使用的语句
products.DataSource = productsByCategory;
productsByCategory作为业务逻辑组件一个方法的返回值,正是实现了IList接口的数据。
接下来看该数据是怎么得到的。
基本的过程都是从最后的数据操作组件开始层层的传递过来,因此我们直接看最后的数据操作部分。
PetShop.SQLServerDAL.Product : IProduct
public IList GetProductsByCategory(string category) {
IList productsByCategory = new ArrayList();
SqlParameter parm = new SqlParameter(PARM_CATEGORY, SqlDbType.Char, 10);
parm.Value = category;
//Execute a query to read the products
using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_PRODUCTS_BY_CATEGORY, parm)) {
while (rdr.Read()){
//向ArrayList里添加一整个的ProductInfo实体
ProductInfo product = new ProductInfo(rdr.GetString(0), rdr.GetString(1), null);
productsByCategory.Add(product);
}
}
return productsByCategory;
}
很明显,返回的是一个ArrayList。这个里面放着该大类的所有小类的资料。一个小类就是一个ProductInfo,而ProductInfo作为一个瘦数据类存放小类的一些基本资料。
现在我们在回头看一下在Category.aspx中SimplePager的部分标记代码
<%# DataBinder.Eval(Container.DataItem, "Id") %>
从数据绑定的知识可以得到,这个id其实是ProductInfo的一个属性。
if(itemsByProduct.Count > 0)
在Items.aspx中进行的数据操作和Category.aspx的基本一样。
Items.aspx.cs里最后有这么一句代码:
productName.Text = ((ItemInfo)itemsByProduct[0]).ProductName;
因为itemsByProduct是一个ArrayList,所以itemsByProduct[0]实际上返回的就是一个ItemInfo。
从上面的分析可以看出,业务逻辑和业务实体分开来的好处。
今天晚上看了近两个小时的购物车,基本把原理弄明白了,先写一个类似结构的类来简单的演示一下
Store类模仿购物车内的物品
public class Store
{
private string name;
private int id;
private DateTime time;
public Store(string name,int id,DateTime time)
{
this.name=name;
this.id=id;
this.time=time;
}
//属性
public string Name
{
get{return this.name;}
}
public int Id
{
get{return this.id;}
}
public DateTime Time
{
get{return this.time;}
}
}
StoreList类模仿购物车
public class StoreList : IEnumerable
{
ArrayList al=new ArrayList();
public StoreList()
{}
//向车内添加物品
public void Add(Store st)
{
this.al.Add(st);
}
//返回全部物品
public ArrayList List
{
get{return this.al;}
}
//实现IEnumerable接口
#region IEnumerable 成员
public IEnumerator GetEnumerator()
{
return this.al.GetEnumerator();
}
#endregion
//添加一个索引器,注意没有判断索引数的合法性
public Store this[int index]
{
get{return (Store)al[index];}
}
//物品的数量
public int Count
{
get{return al.Count;}
}
}
最后的演示页面
public class TestStore : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Button addStore;
protected System.Web.UI.WebControls.Label showMsg;
private void Page_Load(object sender, System.EventArgs e)
{
show();
}
#region Web 窗体设计器生成的代码
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: 该调用是 ASP.NET Web 窗体设计器所必需的。
//
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// 设计器支持所需的方法 - 不要使用代码编辑器修改
/// 此方法的内容。
/// </summary>
private void InitializeComponent()
{
this.addStore.Click += new System.EventHandler(this.addStore_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
//点击添加按钮后的处理事件,向保存在Session中的购物车添加一个商品
private void addStore_Click(object sender, System.EventArgs e)
{
Store st=new Store("alex",0,DateTime.Now);
//检查Session内是否存有购物车,如没有,则添加一个
if(Session["stxx"]==null)
{
StoreList sl=new StoreList();
Session["stxx"]=sl;
}
//从Session中得到购物车,然后向里面添加一个商品
StoreList sls=(StoreList)Session["stxx"];
sls.Add(st);
//注意这里,最后分析这个
//Session["stxx"]=sls;
}
//展示购物车内的商品
private void show()
{
StringBuilder sb=new StringBuilder();
if(Session["stxx"]!=null)
{
StoreList sls=(StoreList)Session["stxx"];
//利用索引循环取出商品
for(int i=0;i<sls.Count;i++)
sb.Append(sls[i].Time.ToString()+"<br>");
showMsg.Text=sb.ToString();
}
}
}
Store是一个瘦实体类,而StoreList是一个可变类。StoreList类通过里面的ArrayList保存Store类,并提供了相应的方法来对Store进行操作。
来看这个:
//从Session中得到购物车,然后向里面添加一个商品
StoreList sls=(StoreList)Session["stxx"];
sls.Add(st);
//注意这里,最后分析这个
//Session["stxx"]=sls;
这里涉及到一个关于Session的问题,由于我们的StoreList保存在了Session中,所以每次操作都要先从Session里把StoreList取出来,但是在操作完后,并没有再把StoreList保存回Session,这主要是因为我们提取出来的并不是Session里保存的值,而只是得到了对Session里保存的值的引用,所以之后的操作其实都是在对Session里保存的值进行,就没有必要最后再保存了。
终于到购物车了,在看这个之前应该已经明白了第三篇的那个模型,这样购物车基本也就明白了。
来看一下ShoppingCart.aspx这个页。
当你看好了一个宠物,比如可爱的Golden Retriever,嘿嘿,那就点add to cart按钮,这时就会跳到ShoppingCart.aspx,url里带了这个宠物的id号,根据该id号程序将该宠物放到cart里面。然后你可以再去挑别的宠物,比如一只猫(……),虽然这不是什么好主意。然后该宠物的id号又会被传到ShoppingCart.aspx,并添加到cart里面。在ShoppingCart.aspx里,你可以更改想要领养的宠物的数量,然后程序会根据你要求的数量来计算所需的钱以及该宠物是否还有剩余。在你做出决定后可以点proceed to checkout进入定单生成的环节。
上面是大体的流程。下面来看.net petshop是怎么实现这个cart的
基本的实现主要是BLL里的Cart和Model里的CartItemInfo,而Web.ProcessFlow的CartControler则负责具体的实现。想一想第三篇里的那个模型,具体到这里,每挑选一个宠物,就有一个CartItemInfo通过CartControler添加到了保存在Session里的Cart里面,最后生成定单的时候就从Session里把Cart的值取出来(CartControler有生成定单的方法,下一篇再说)。
来看一下ShoppingCart.aspx.cs里向Cart添加CartItemInfo的代码
// Create an instance of the cart controller
ProcessFlow.CartController cartController = new ProcessFlow.CartController();
myCart = cartController.GetCart(true);
if (!Page.IsPostBack){
// Get the itemdId from the query string
string itemId = Request["itemId"];
if (itemId != null){
// Clean the input string
itemId = WebComponents.CleanString.InputText(itemId, 50);
myCart.Add(itemId);
cartController.StoreCart(myCart);
}
}
先看这一句 myCart = cartController.GetCart(true);
GetCart方法用来生成一个Cart,它是先在Session里检查,如Session里没有保存Cart,就生成一个新的,否则就把保存在Session里的Cart取出来,然后使用Add方法把新的宠物加到Cart里。
public Cart GetCart(bool create){
// Fetch the cart object from session state
Cart myCart = (Cart)HttpContext.Current.Session[CART_KEY];
if ( myCart == null ){
if (create){
myCart = new Cart();
}else{
HttpContext.Current.Server.Transfer(URL_NOCART);
return null;
}
}
return myCart;
}
下面是Add方法的实现
public void Add(string ItemId) {
// _items是在Cart里保存的宠物集合,通过遍历来判断这是否是一个新类别
foreach (CartItemInfo cartItem in _items) {
if (ItemId == cartItem.ItemId) {
cartItem.Quantity++;
cartItem.InStock = (GetInStock(ItemId) - cartItem.Quantity) >= 0 ? true : false;
_total = _total+(cartItem.Price*cartItem.Quantity);
return;
}
}
//是一个新类别,则把一个CartItemInfo加入Cart,可以在这里看到CartItemInfo的组成
Item item = new Item();
ItemInfo data = item.GetItem(ItemId);
CartItemInfo newItem = new CartItemInfo(ItemId,data.Name, (data.Quantity >= 1), 1, (decimal)data.Price);
_items.Add(newItem);
_total = _total+(data.Price);
}
这就完成了添加。然后是数量的更改。
// Check for update button
if (e.CommandName == CMD_UPDATE){
TextBox txt;
int qty;
int index;
// Go through each item on the page
for (int i = 0, j = cart.Items.Count; i < j; i++){
// lookup the control
txt = (TextBox)cart.Items[i].FindControl(ID_TXT);
try{
qty = int.Parse(txt.Text);
index = cart.CurrentPageIndex * cart.PageSize + i;
// If the new qty is zero, remove the item from the cart
if (qty <= 0)
myCart.RemoveAt(index);
// Update the item with the new quantity
else
myCart[index].Quantity = qty;
}
catch {}
}
}else
// otherwise the command is to remove the an item
myCart.Remove((string)e.CommandArgument);
因为宠物的添加是先于数量的更改进行的,所以到了这里Session里肯定有保存Cart。数量的更改直接同过索引器来完成,更改后直接就会保存。Remove和RemoveAt都实现了从Cart删除指定的CartItemInfo。
ShoppingCart.aspx页还有其他的东西,比如一个继承自SimplePager的自定义控件,,还有判断是否显示用户喜好的宠物列表的判断。
Refresh方法用来重新计算total的值,哈,不过我不清楚微软究竟想拿这个值显示什么?在我下的这个版本里,根本就不是subtotla的总和,而是price的总和,但问题在于,当你把一种宠物从Cart里移除的时候,它竟然会total=total-subtotal,因此常常会出现负数……
购物车从开始到最后销毁,都是在和Session打交道,没有任何与数据库的交互。我不是很了解面向对象技术,但我觉得oo在这里得到了很好的体现。
点proceed to checkout后,就进入Checkout.aspx,确认后进入OrderBilling.aspx,在这里可以修改你的信息,完成后点continue,会出现个人信息的只读页面,最终确认后就进入OrderProcess.aspx,在这里是定单的详细情况,并且是只读的,到这里,定单被添加到数据库,购物完成。
Checkout.aspx把数据从Session中取出来,然后显示到页面,没什么好说的。
OrderBilling.aspx,这个页面一开始显示的信息可写,我们看OnLoad事件中,是用ProcessFlow.AccountController.GetAccountInfo获得用户的信息CreditCardInfo,然后显示在一些可写的控件中,比如用户控件:StaticAddress。当点击确认后,使用StoreCreditCard把刚才获得的用户信息保存到Sessin,准备呆会用。
OrderProcess.aspx是最终的定单生成页面。主要就是一个方法:
ProcessFlow.CartController.PurchaseCart 来看它的实现
public OrderInfo PurchaseCart(){
// Fetch the cart from session
Cart myCart = (Cart)HttpContext.Current.Session[CART_KEY];
// Make some checks on the cart
if ( ( myCart == null ) || ( myCart.Count==0 ) ) {
HttpContext.Current.Server.Transfer(URL_NOCART);
//HttpContext.Current.Response.Redirect(URL_NOCART, false);
return null;
}else{
// Build up the order
OrderInfo newOrder = new OrderInfo();
newOrder.UserId = ((AccountInfo)HttpContext.Current.Session[ACCOUNT_KEY]).UserId;
newOrder.CreditCard = (CreditCardInfo)HttpContext.Current.Session[CREDITCARD_KEY];
newOrder.BillingAddress = (AddressInfo)HttpContext.Current.Session[BILLING_KEY];
newOrder.ShippingAddress = (AddressInfo)HttpContext.Current.Session[SHIPPING_KEY];
newOrder.LineItems = (LineItemInfo[])myCart.GetOrderLineItems().ToArray(typeof(LineItemInfo));
newOrder.OrderTotal = myCart.Total;
newOrder.Date = DateTime.Now;
// Send the order to the middle tier
OrderInsert order = new OrderInsert();
//向数据库插入数据
newOrder.OrderId = order.Insert(newOrder);
// clear the session objects used
HttpContext.Current.Session[CART_KEY] = null;
HttpContext.Current.Session[CREDITCARD_KEY] = null;
HttpContext.Current.Session[BILLING_KEY] = null;
HttpContext.Current.Session[SHIPPING_KEY] = null;
return newOrder;
}
}
Order主要是由保存在Session里的值形成,其中包括购物车。
在Order有一个LineItemInfo,它是由Cart.GetOrderLineItems方法依据cart里CartItemInfo的值返回的,是定单的物品部分,和CartItemInfo相比,主要是多了一个Line属性,这个Line是用来表示物品在定单内的序号。
下面是Insert的实现。
public int Insert(OrderInfo order) {
// Get an instance of the Order DAL using the DALFactory
IOrder dal = PetShop.DALFactory.Order.Create();
// Call the insert method in the DAL to insert the header
int orderId = dal.Insert(order);
// Get an instance of the Inventory business component
Inventory inventory = new Inventory();
//向数据库插入库存信息
inventory.TakeStock( order.LineItems);
// As part of the sample application we have created a user
// you can tested distributed transactions with
// If the order has been created with the user 'Acid',
// then throw an exception which will rollback the entire transaction
if (order.UserId == ACID_USER_ID)
throw new ApplicationException(ACID_ERROR_MSG);
// Set the orderId so that it can be returned to the caller
return orderId;
}
大体就这么多了,表示层的东西到这里基本上就算全看完了,嘿嘿
还记得用户注册时收集信息的方式吗?下面这几句:
//......
AddressInfo address = addr.Address;
//.....
AccountInfo accountInfo = new AccountInfo(userId, password, email, address, language, favCategory, showFavorites, showBanners);
//.....
ccountController.CreateAccount(accountInfo)
AddressInfo和AccountInfo都是瘦实体类,整个程序都用这种模型来完成信息传递。这些类的定义都在Model组件中。应该是整个petshop3.0中最重要的组件之一。
Model:瘦数据类或业务实体,在应用程序各层之间传递数据的瘦数据类。这些是用 C# 类实现的,每个字段都以属性的形式公开。 每个类都标记为“serializable”,启用进程间传输。
.net是面向对象的,这大家都知道,可以在具体实现的时候应该怎么做呢?总之我是完全的不明白。不过看了petshop3.0中Model组件的应用,算是又长了见识。
在程序中总有那么一些信息可以归为一类,比如商品信息,用户信息……拿用户信息来说,应该有用户名、密码、住址、电话、个人爱好等具体的信息。在petshop3.0,这些就可以总归为一个实体,而其中的信息都是这个类的属性,在其中住址、电话等信息又可以单独的归为一个实体,因为他们是描述个人具体信息的,并且是可变的(描述的不是很准确,可能有错误)。所以我们看到petshop3.0把这些信息归成一个AddressInfo实体,而该实体又成为AccountInfo实体的一个属性。具体应用的时候,这些实体都是作为一个整体使用,因为你总不能把一个人切开吧?再从c#的角度看,这些都是类型,可以看成用户的自定义类型,所以我们看到AddressUI这个用户控件把AddressInfo做为属性对外公开。
实体模型的使用贯穿了整个petshop3.0,从表面看,几乎每一层都引用了Model组件。还是以用户这块来说下去,表示层收集数据的时候,会把用户填的信息作为一个实体来收集,完成后用户接口处理组件会把信息实体做为参数传送给业务逻辑组件,业务逻辑部分再把信息实体做为参数经工厂传送给数据处理组件,在这里做最后处理,提交到数据库或返回错误。
在整个过程中,根本就不存在单独的值类型的传递。这样做的好处很明显,使逻辑操作更加透明和易于理解,便于构建复杂系统的模型,可以封装一些不需要向程序公开的信息,等等。
此类代码都处都是,一个比较明显的例子就是CartItemInfo,该实体模型表述放到购物车内的商品。该实体并不保存到数据库,完全是存在于进程中的。由于该实体的存在,大大简化了购物车这一抽象实体的组成。
实际上,在Model组件中根本就没有Cart这个实体。Cart的实现由BLL组件来完成。这个Cart并非和上面一样的瘦实体类,而是另外一种实体模型,因为它不仅包含着作为实体本身的信息,而且还有自己的行为。按照msdn上的叫法,这是“带有CRUD行为的业务实体”。我们可以把这个Cart看成一个智能机器人,他会自己挑选商品,并给出现在车内的信息。
通过类图来对比一下Cart和CartItemInfo。
CartItemInfo只有属性,而Cart则包括属性和方法。
要注意的是,Cart归属于业务逻辑组件,这是因为它的作用更多的是执行其行为,它的最主要的属性就是一个索引器,通过这个来获得CartItemInfo。
最后看一下CartItemInfo的代码,了解此类模型的基本构成。
/// <summary>
/// Business entity used to model items in a shopping cart
/// </summary>
[Serializable]
public class CartItemInfo {
private const string YES = "Yes";
private const string NO = "No";
// Internal member variables
private int _quantity = 1;
private bool _inStock = false;
private string _itemId = null;
private string _name;
private decimal _price;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="itemId">Every cart item requires an itemId</param>
public CartItemInfo(string itemId) {
this._itemId = itemId;
}
/// <summary>
/// Constructor with specified initial values
/// </summary>
/// <param name="itemId">Id of item to add to cart</param></param>
/// <param name="name">Name of item</param>
/// <param name="inStock">Is the item in stock</param>
/// <param name="qty">Quantity to purchase</param>
/// <param name="price">Price of item</param>
public CartItemInfo(string itemId, string name, bool inStock, int qty, decimal price) {
this._itemId = itemId;
this._name = name;
this._quantity = qty;
this._price = price;
this._inStock = inStock;
}
// Properties
public int Quantity {
get { return _quantity; }
set { _quantity = value; }
}
public bool InStock {
get { return _inStock; }
set { _inStock = value; }
}
public decimal Subtotal {
get { return (decimal)(this._quantity * this._price); }
}
public string ItemId {
get { return _itemId; }
}
public string Name {
get { return _name; }
}
public decimal Price {
get { return _price; }
}
}
下面是msdn上关于业务实体的一些资料。顺便转过来好了:)
----原文地址------
定义自定义业务实体组件
表示业务实体的自定义类通常包含以下成员:
用于在本地缓存业务实体的数据的专用字段。这些字段在数据访问逻辑组件从数据库检索数据时保存数据库数据的一个快照。
用于访问实体的状态和访问实体内数据的子集及层次结构的公共属性。这些属性的名称可以与数据库的列名称相同,但这并不是一个绝对要求。可以根据您的应用程序的需要选择属性名,而不必使用数据库中的名称。
用以使用实体组件中的数据执行本地化处理的方法和属性。
用以通知实体组件内部状态变化的事件。
图 9 所示为使用自定义实体类的方法。注意,实体类并不知道数据访问逻辑组件或基础数据库;所有数据库访问都由数据访问逻辑组件执行,以集中数据访问策略和业务逻辑。此外,在层间传递业务实体数据的方式与表示业务实体的格式也没有直接关系;例如,可以在本地将业务实体表示为对象,而用另一种方法(如标量值或 XML)将业务实体数据传递到其他层。
图 9:自定义业务实体组件的作用(单击缩略图以查看大图像)
定义自定义业务实体组件的建议
在实现自定义实体组件时,请考虑以下建议:
选择使用结构还是使用类。对于不包含分层数据或集合的简单业务实体,可以考虑定义一个结构来表示业务实体。对于复杂的业务实体或要求继承的业务实体,可将实体定义为类。有关结构和类这两种类型的比较,请参阅 Structures and Classes。
表示业务实体的状态。对于数字、字符串等简单值,可以使用等价的 .NET 数据类型来定义字段。有关说明如何定义自定义实体的代码示例,请参阅附录中的如何定义业务实体组件。
表示自定义业务实体组件中的子集合和层次结构。表示自定义实体中的数据的子集合和层次结构的方法有两种:
.NET 集合(例如 ArrayList)。.NET 集合类为大小可调的集合提供了一个方便的编程模型,还为将数据绑定到用户界面控件提供了内置的支持。
DataSet。DataSet 适合于存储来自关系数据库或 XML 文件的数据的集合和层次结构。此外,如果需要过滤、排序或绑定子集合,也应首选 DataSet。
有关说明如何表示自定义实体中数据的集合和层次结构的代码示例,请参阅附录中的如何表示自定义实体中数据的集合和层次结构。
支持用户界面客户端的数据绑定。如果自定义实体将要由用户界面使用并且希望利用自动数据绑定,可能需要在自定义实体中实现数据绑定。请考虑以下方案:
Windows 窗体中的数据绑定。您可以将实体实例的数据绑定到控件而不必在自定义实体中实现数据绑定接口。也可以绑定实体的数组或 .NET 集合。
Web 窗体中的数据绑定。如果不实现 IBindingList 接口,则不能将实体实例的数据绑定到 Web 窗体中的控件。然而,如果只想绑定集合,则可以使用数组或 .NET 集合而不必在自定义实体中实现 IBindingList 接口。
有关说明如何将自定义实体绑定到用户界面控件的代码示例,请参阅附录中的如何将业务实体组件绑定到用户界面控件。
公开内部数据更改的事件。公开事件可以获得丰富的客户端用户界面设计,因为它使得无论数据显示在哪里都可以对其进行刷新。事件应当只针对内部状态,而不是针对服务器上的数据更改。有关说明如何公开自定义实体类中的事件的代码示例,请参阅附录中的如何公开业务实体组件中的事件。
使业务实体可序列化。使业务实体可序列化可以将业务实体的状态保持在中间状态而不进行数据库交互。这样可以方便脱机应用程序的开发和复杂用户界面过程的设计,即在完成前不会影响业务数据。序列化有两种类型:
使用 XmlSerializer 类进行 XML 序列化。如果只需要把公共字段和公共读/写属性序列化为 XML,则可以使用 XML 序列化。注意,如果从 Web 服务返回业务实体数据,对象将通过 XML 序列化自动序列化为 XML。
您可以对业务实体执行 XML 序列化而无需在实体中实现任何附加代码。然而,只有对象中的公共字段和公共读/写属性被序列化为 XML。专用字段、索引生成器、专用属性、只读属性及对象图不会被序列化。您可以使用自定义实体中的属性控制结果 XML。有关将自定义实体组件序列化为 XML 格式的详细信息,请参阅附录中的如何将业务实体组件序列化为 XML 格式。
使用 BinaryFormatter 或 SoapFormatter 类进行格式序列化。如果需要序列化对象的所有公共字段、专用字段及对象图,或者需要与远程服务器之间传递实体组件,则可以使用格式序列化。
格式类将序列化对象的所有公共和专用字段及属性。BinaryFormatter 将对象序列化为二进制格式,SoapFormatter 将对象序列化为 SOAP 格式。使用 BinaryFormatter 的序列化比使用 SoapFormatter 的序列化速度要快。要使用任何一个格式类,都必须将实体类标记为 [Serializable] 属性。如果需要显式控制序列化格式,您的类还必须实现 ISerializable 接口。有关如何使用格式序列化的详细信息,请参阅附录中的如何将业务实体组件序列化为二进制格式及如何将业务实体组件序列化为 SOAP 格式。
注意:还原序列化某个对象时,不会调用默认的构造函数。对还原序列化添加这项约束,是出于性能方面的考虑。
定义自定义实体的优点如下:
代码易读。要访问自定义实体类中的数据,可以使用有类型的方法和属性,如以下代码所示:
// 创建一个 ProductDALC 对象
ProductDALC dalcProduct = new ProductDALC();
// 使用该 ProductDALC 对象创建和填充一个 ProductEntity 对象。
// 此代码假设 ProductDALC 类有一个名为 GetProduct 的方法,
// 该方法使用 Product ID 作参数(本例中为 21),并返回一个
// 包含该产品的所有数据的 ProductEntity 对象。
ProductEntity aProduct = dalcProduct.GetProduct(21);
// 更改该产品的产品名称
aProduct.ProductName = "Roasted Coffee Beans";
在上述示例中,产品是一个名为 ProductEntity 的自定义实体类的一个实例。ProductDALC 类有一个名为 GetProduct 的方法,后者创建一个 ProductEntity 对象,将某个特定产品的数据填充到该对象,然后返回 ProductEntity 对象。调用应用程序可以使用 ProductName 等属性访问 ProductEntity 对象中的数据,并且可以调用方法以操作该对象。
封装。自定义实体可以包含方法以封装简单的业务规则。这些方法操作缓存在实体组件中的业务实体数据,而不是访问数据库中的实时数据。请考虑以下示例:
// 调用一个在 ProductEntity 类中定义的方法。
aProduct.IncreaseUnitPriceBy(1.50);
在上述示例中,调用应用程序对 ProductEntity 对象调用一个名为 IncreaseUnitPriceBy 的方法。在调用应用程序对 ProductDALC 对象调用相应方法,从而将 ProductEntity 对象保存到数据库之前,这一更改并不是永久性的。
构建复杂系统的模型。在构建复杂域问题(在不同业务实体之间存在很多交互)的模型时,可以定义自定义实体类,从而将复杂性隐藏在经过很好定义的类接口的后面。
本地化验证。自定义实体类可以在其属性存取器中执行简单的验证测试以检测无效的业务实体数据。有关详细信息,请参阅如何在业务实体组件的属性存取器中验证数据。
专用字段。您可以隐藏不希望向调用程序公开的信息。
定义自定义实体的缺点如下:
业务实体集合。自定义实体表示的是单个业务实体,而不是一个业务实体集合。要保存多个业务实体,调用应用程序必须创建一个数组或一个 .NET 集合。
序列化。您必须在自定义实体中实现自己的序列化机制。可以使用属性来控制实体组件的序列化方式,也可以通过实现 ISerializable 接口来控制自己的序列化。
表示业务实体中的复杂关系和层次结构。您必须在业务实体组件中实现自己的关系和层次结构表示机制。如前面所述,DataSet 通常是实现这一目的的最简单方式。
搜索和排序数据。您必须定义自己的机制来支持实体的搜索和排序。例如,可以通过实现 IComparable 接口以便将实体组件保存在一个 SortedList 集合或 Hashtable 集合中。
部署。您必须在所有物理层部署包含自定义实体的程序集。
支持企业服务 (COM+) 客户端。如果一个自定义实体将由 COM+ 客户端使用,则必须为包含该实体的程序集提供一个严格名称,并且必须在客户端计算机上注册。通常,该程序集安装在全局程序集缓存中。
可扩展性问题。如果修改了数据库架构,则可能需要修改自定义实体类并重新部署程序集。
定义带有 CRUD 行为的自定义业务实体组件
在定义一个自定义实体时,可以提供方法以完全封装对基础数据访问逻辑组件的 CRUD 操作。这是比较传统的面向对象的方法,可能适用于复杂的对象域。客户端应用程序不再直接访问数据访问逻辑组件类,而是创建一个实体组件并对该实体组件调用 CRUD 方法。这些方法将调用基础的数据访问逻辑组件。
图 10 所示为带有 CRUD 行为的自定义实体类的作用。
图 10:带有 CRUD 行为的自定义业务实体组件的作用(单击缩略图以查看大图像)
定义带有 CRUD 行为的自定义实体类的优点如下:
封装。自定义实体可以封装由基础数据访问逻辑组件定义的操作。
与调用程序的接口。调用程序必须只处理一个接口来保持业务实体数据。不必直接访问数据访问逻辑组件。
专用字段。您可以隐藏不希望向调用程序公开的信息。
定义带有 CRUD 行为的自定义实体类的缺点如下:
处理业务实体集合。自定义实体中的方法属于单个业务实体实例。要支持业务实体集合,可以定义静态方法以读取或返回一个数组或一个实体组件集合。
开发时间长。传统的面向对象方法通常比使用现有对象(如 DataSet)需要更多的设计和开发工作。
SimplePager是继承自Repeater的一个自定义控件,在整个站中应用很广,下面通过Category.aspx这个页面具体的研究研究:)
先来看Category.aspx.cs,很奇怪吧,怎么没有和Load事件相关的方法呢?只有一个控制分页的PageChanged,而这个分页方法也有问题,每次都是获得缓存里的数据,然后改变的只CurrentPageIndex属性而已,并且该方法接受的参数竟然是DataGridPageChangedEventArgs,真晕,这个控件是一个Repeater嘛……
下面就让我们打开SimplePager.cs来看个究竟吧~~(两天的感冒刚好,目前精神百倍中……),其实看这个的时候最好是用.NET Reflector来查看,这样比较容易好辨认(我现在的电脑就把打开dll文件的默认程序设成了.NET Reflector)。
先来看第一个问题,在具体的页面中没有Load事件,那肯定是在控件的内部做了手脚
override protected void OnLoad(EventArgs e) {
if (Visible) {
string page = Context.Request[KEY_PAGE];
int index = (page != null) ? int.Parse(page) : 0;
SetPage(index);
}
}
当Load事件发生时就调用SetPage方法
public void SetPage(int index) {
//对DataGridPageChangedEventArgs有疑惑的可以看文章的最后
OnPageIndexChanged(new DataGridPageChangedEventArgs(null, index));
}
然后是OnPageIndexChanged
virtual protected void OnPageIndexChanged(DataGridPageChangedEventArgs e) {
if (PageIndexChanged != null)
PageIndexChanged(this, e);
}
PageIndexChanged是一个事件,使用了DataGridPageChangedEventHandler作为它的委托
public event DataGridPageChangedEventHandler PageIndexChanged;
这样就不难理解了,PageChanged就是在页面加载时的“Page_Load”方法,并且还是页数改变时相应的处理方法。换句话说,每次进入该页,程序都当成页数改变事件来处理。
从上面我们可以看到一个问题,即在这三个连续的方法中页数的传递都是通过
string page = Context.Request[KEY_PAGE];这里获得的值来进行的,好象CurrentPageIndex属性根本就没有用……
CurrentPageIndex的作用主要体现在分页方法这里
override protected void OnDataBinding(EventArgs e) {
//Work out which items we want to render to the page
int start = CurrentPageIndex * pageSize;
int size = Math.Min(pageSize, ItemCount - start);
IList page = new ArrayList();
//Add the relevant items from the datasource
for (int i = 0; i < size; i++)
page.Add(dataSource[start + i]);
//set the base objects datasource
base.DataSource = page;
base.OnDataBinding(e);
}
OnDataBinding是当服务器控件绑定到数据源时发生的DataBinding事件的处理方法。在这里有机会对数据源进行处理。而这里的确就是对数据源进行了处理,取到当前页的数据并把他们作为新的数据源赋给控件。
for (int i = 0; i < size; i++)
page.Add(dataSource[start + i]);
因为dataSource是实现了IList接口的数据,所以可以这样用索引器来操作数据,再往里面看,这些数据是什么呢?好象以前讨论过这个,数据源实际是一个ArrayList,里面放的是ProductInfo实体。
看到这里,或许你和我一样,会有这样的疑惑,既然在Load阶段就接收到了一个页数,为什么在这里还要另外的用一个,直接用一个不可以吗?我在这里的看法是,这样可以使程序的可读性更高,并且使控件的用户接口更加友好。
或者,那个变量根本就传不到OnDataBinding方法里面?这个我没有调试过,感兴趣的可以试试。
除了这里,Render方法也用CurrentPageIndex来实现上一页和下一页连接的显示。
顺便说一下ViewStatePager,这个是SimplePager的子类,主要的区别是该控件识别页数时使用了ViewState来完成,这样不至于和同一页面上的其他需要分页的控件(比如SimplePager)冲突。
最后说一个小发现
public DataGridPageChangedEventArgs(object commandSource, int newPageIndex)
{
this.commandSource = commandSource;
this.newPageIndex = newPageIndex;
}
这是通过.NET Reflector得到的DataGridPageChangedEventArgs的构造函数
public int NewPageIndex
{
get
{
return this.newPageIndex;
}
}
这是通过.NET Reflector得到的DataGridPageChangedEventArgs的NewPageIndex属性的定义。
看一下SQLServerDAL,前面有高人说了,那个SQLHelper类似于daab for .net,因此在这里我就不多话了,对daab for .net的讨论可以参看msdn的相关文章。
来看Account.cs,里面有一个GetAddress方法,用来通过用户名获得用户资料。在这里面主要是分两步,第一步是获得参数并给其赋值,第二步是使用SqlDataReader来从数据库取值。
public AddressInfo GetAddress(string userId) {
AddressInfo address= null;
//获得参数列表
SqlParameter[] addressParms = GetAddressParameters();
//给参数赋值
addressParms[0].Value = userId;
//使用SqlDataReader来从数据库取值
using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_ADDRESS, addressParms)) {
if (rdr.Read()) {
address = new AddressInfo(rdr.GetString(0), rdr.GetString(1), rdr.GetString(2), rdr.GetString(3), rdr.GetString(4), rdr.GetString(5), rdr.GetString(6), rdr.GetString(7), rdr.GetString(8));
}
}
return address;
}
参数的获得比较复杂,先来看SqlDataReader的使用吧
using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_ADDRESS, addressParms)) {
if (rdr.Read()) {
address = new AddressInfo(rdr.GetString(0), rdr.GetString(1), rdr.GetString(2), rdr.GetString(3), rdr.GetString(4), rdr.GetString(5), rdr.GetString(6), rdr.GetString(7), rdr.GetString(8));
}
}
很简单的应用,SQLHelper.ExecuteReader的确是起到了和daab for .net差不多的作用。因为是获得个人资料,所以这里只有一条记录,就算是要处理需要分页的大数据量时,它也是这样用SqlDataReader返回所有的记录,但是之后就会把这些记录全部保存在Cache中,这样以后就不用查询数据库了。
GetAddressParameters方法用来获得此次操作需要的参数,它也是使用了SQLHelper的相关方法来完成的。
private static SqlParameter[] GetAddressParameters() {
//如果以前有保存参数,那就取出来
SqlParameter[] parms = SQLHelper.GetCachedParameters(SQL_SELECT_ADDRESS);
if (parms == null) {
parms = new SqlParameter[] {
new SqlParameter(PARM_USER_ID, SqlDbType.VarChar, 80)};
//创建新的参数数组,然后“缓存”起来
SQLHelper.CacheParameters(SQL_SELECT_ADDRESS, parms);
}
return parms;
}
我们可以接着看SQLHelper中的细节部分
//parmCache的定义
private static Hashtable parmCache = Hashtable.Synchronized(new Hashtable());
//“缓存”参数
public static void CacheParameters(string cacheKey, params SqlParameter[] cmdParms) {
parmCache[cacheKey] = cmdParms;
}
/// <summary>
/// Retrieve cached parameters
/// </summary>
/// <param name="cacheKey">key used to lookup parameters</param>
/// <returns>Cached SqlParamters array</returns>
public static SqlParameter[] GetCachedParameters(string cacheKey) {
//从缓存中取出值
SqlParameter[] cachedParms = (SqlParameter[])parmCache[cacheKey];
if (cachedParms == null)
return null;
SqlParameter[] clonedParms = new SqlParameter[cachedParms.Length];
//使用clone通过遍历得到一个新的参数数组
for (int i = 0, j = cachedParms.Length; i < j; i++)
clonedParms[i] = (SqlParameter)((ICloneable)cachedParms[i]).Clone();
return clonedParms;
}
用来缓存参数的parmCache实际上一个private static Hashtable,因为是static,所以自然是独一无二的了,创建一次就可以永远使用,起到和缓存差不多的效果。
在返回参数时要用clone来创建一个新的参数数组返回,是因为parmCache只有一个,而返回的参数数组则至少一个人用一个。更为重要的是,现在保存在parmCache的参数列表都是没有具体值的参数,而返回后则根据每个用户的不同情况而被赋于不同的值。
从这里子可以看出,有一个自己的daab是多么的重要。先前在论坛看到过有人喜欢用拖的方式来进行数据库的连接,那种做法我觉得不是很好,手写代码很重要的,写一个自己顺手的daab更是重要。
ms介绍
为了简化数据库访问类的使用,我们选择 GoF (译注:指 Erich Gamma 等著《设计模式》一书)概述的工厂设计模式,通过反射动态在运行时加载正确的数据访问对象。 工厂设计模式是这样实现的: 创建一个 C# 接口,其中对于数据库访问类必须公开的每个方法都要声明一个方法。 对于每一个要支持的数据库,都创建一个实现数据库特定代码的具体类,以执行接口也称“协定”中的每一项操作。 为了支持运行时确定加载哪一个具体类,需要创建第三个类,也就是工厂类,它从配置文件中读入一个值以确定应该使用反射加载哪一个程序集。 通过 .NET 的反射命名空间,可以加载某个特定程序集并用该程序集创建某个对象的实例。 为了使应用程序更安全,为版本控制提供更好的支持,我们可以在应用程序配置文件(也就是这里的 web.config. )中添加要加载的程序集文件的“证据”,这意味着 .NET 框架将只加载我们在编译期间签过名而且有正确版本号的程序集。 图 10 说明了业务逻辑类、工厂类和数据库访问类是如何相互操作的。 这一创建的解决方案最重要的优势是数据库访问类可以在业务逻辑类之后编译,只要数据访问类实现了 IDAL 接口。 这意味着,如果要创建应用程序的 DB2 版本,我们不需要改动业务逻辑层(或者 UI 层)。 创建 DB2 兼容版本的步骤如下:
1.创建 DB2 的数据库访问类,它应该实现 IDAL 接口。
2.将 DB2 访问类编译成一个程序集。
3.测试和部署新的数据程序集到一台运行中的服务器上。
4.更改配置文件,指向新的数据库访问类。
无需更改或重新编译业务逻辑组件。
图 10. .NET Pet Shop 中 DAL 工厂类的实现
2.数据分析
5 PetShop.DALFactory 数据访问工厂
工厂模式是设计模式的一种,以我理解就像Factory这个词一样,对于用户来说,工厂里产品如何生产的你不用知道,你只要去用工厂里生产出来的东西就可以了。MSPetShop3.0用工厂模式来实现了对SqlServer和Oracle数据库访问的操作,而用户(business Logic Layer)不用知道也不用关心后台用的是哪一种数据库,它只要用接口就行了,接口中定义了要用的方法,当调用接口时会根据具体的情况再去调用底层数据访问操作。而现在这个DALFactory就是关键,当BLL层要操作数据库时,DALFactory会根据具体情况再去使用本文上面介绍的SqlServerDAL和OracleDAL中的一个。这样系统上层只管调用,而下层来实现细节,上级只管发号施令,下级去干活。对于上层来说实现细节被隐藏了。
那么DALFactory是如何决定应该用SqlServerDAL还是用OracleDAL的呢?我们接着分析。
以下是PetShop.DALFactory.Account类的实现:
namespace PetShop.DALFactory {
/// <summary>
/// Factory implementaion for the Account DAL object
/// </summary>
public class Account
{
public static PetShop.IDAL.IAccount Create()?//<<<<?----这里返回接口
{?
/// Look up the DAL implementation we should be using
string path = System.Configuration.ConfigurationSettings.AppSettings["WebDAL"];
string className = path + ".Account";
// Using the evidence given in the config file load the appropriate assembly and class
return (PetShop.IDAL.IAccount) Assembly.Load(path).CreateInstance(className);
}
}
}
以下则是web.config中<appSettings>节点中的一部分:
<add key="WebDAL" value="PetShop.SQLServerDAL" />
<add key="OrdersDAL" value="PetShop.SQLServerDAL" />?
<add key="Event Log Source" value=".NET Pet Shop" />
上面的Create()方法返回IAccount接口,用System.Configuration.ConfigurationSettings.AppSettings["WebDAL"];则可以得到Web.config的<appsettings>节点中的关于系统中应该使用哪个数据访问层(SqlserverDAL还是OracleDAL)的信息。因为我在安装PetShop3.0时选择的是Sqlserver所以在此是:value="PetShop.SQLServerDAL",如果用的是Oracle那就是value="PetShop.OracleDAL" 了吧!而且这个文件也应该是可以更改的。接下来className=path+”.Account”返回的应该是PetShop.SQLServerDAL.Account,然后再用Assembly.Load加载PetShop.SQLServerDAL.dll,同时创建PetShop.SQLServerDAL.Account的实例,并以接口(PetShop.IDAL.IAccount)类型返回。这样BLL调用IAccount接口时就会用PetShop.SQLServerDAL.Account类的实现代码。(回上面第4再看一下)
看!这样根据系统当前Web.config文件的配置描述(这也应该是系统运行时实际的配置),BLL层只要像下面这样:
// Get an instance of the account DAL using the DALFactory
IAccount dal = PetShop.DALFactory.Account.Create();
AccountInfo account = dal.SignIn(userId, password);//<<?----看看上面第4点的IAccount接口
就可以直接调用接口方法通过下层DAL层操作数据库了(在此具体为用户账号相关操作),而BLL层并不用知道应该通过SqlserverDAL还是OracleDAL访问数据库,这由都DAL Factory决定,你用的是什么数据库以及底层细节,更不用BLL知道,这样做的好处是对于BLL层以及更上层的程序不会或很少机率会因为底层程序变动影响,因为BLL层中调用接口就行了,只要那个接口定义没变,一切仍然OK.
3.相关sdk文档资料。
Assembly 类
定义一个 Assembly,它是可重用、无版本冲突并且可自我描述的公共语言运行库应用程序构造块。
有关此类型所有成员的列表,请参阅 Assembly 成员。
[Visual?Basic]
<Serializable>
<ClassInterface(ClassInterfaceType.AutoDual)>
Public Class Assembly
Implements IEvidenceFactory, ICustomAttributeProvider, _
ISerializable
public static Assembly Load(string assemblyString);
描述
通过给定程序集的长格式名称加载程序集。
参数
assemblyString
程序集名称的长格式。
返回值
加载的程序集。
public object CreateInstance(string typeName);
描述
使用区分大小写的搜索,从此程序集中查找指定的类型,然后使用系统激活器创建它的实例。
参数
typeName
要查找的类型的 Type.FullName。
返回值
表示该类型的 Object 的实例,其区域性、参数、联编程序和激活属性设置为空引用(Visual Basic 中为 Nothing),并且 BindingFlags 设置为 Public 或 Instance,或者设置为空引用 (Nothing)(如果没有找到 typeName)。