使用 LINQ to SQL 进行企业应用程序架构






4.73/5 (40投票s)
2007年10月2日
22分钟阅读

304195

4201
关于在企业应用程序中使用 LINQ to SQL 的架构模式及其性能影响的讨论
- 下载演示解决方案 (VS 2008 beta 2 版本) - 365.4 KB
- 下载演示解决方案 (Orcas beta 1 版本) - 207.1 KB
- 下载数据库脚本 - 614.9 KB
- 下载性能测试解决方案 - 36.5 KB
- 下载性能统计 - 2.9 KB
目录
引言
关于 LINQ 和 DLinq(后来更名为 LINQ to SQL)的在线资源非常丰富。然而,在撰写本文时,我只能找到演示 UI 和数据库之间直接交互的示例,其中查询嵌入在代码隐藏文件中。在本文和随附代码中,我将演示在更高级的企业应用程序场景中使用 DLinq。我无意提供一个可能的模式目录。我的重点是两种流行的架构技术:经典的“三层架构”和新出现的 MVP(模型-视图-呈现器)架构。我无意提供另一个 DLinq 教程。Scott Guthrie 的博客是了解 DLinq 概览的好资源。在本文中,我将介绍 ASP.NET 和 DLinq 的更高级概念,以及 MVP 模式。
随附代码包含多层基础设施组件以及两个网站。第一个网站演示了一个具有 CRUD 功能的三层企业应用程序,它在数据访问层使用 DLinq。此演示网站中的客户列表页面使用 RAD 方法,通过将 GridView 绑定到 ObjectDataSource 控件。详细信息/编辑和插入网页使用代码隐藏模型以及 FormView 等 UI 控件来实现客户实体上的 DML 操作,并报告该客户的五个最近订单统计信息。本文的 A 部分详细描述了该设计。
第二个网站(恰当地命名为 MVPWeb)在其架构中融入了 MVP 模式。更具体地说,它使用了 MVP 的一个变体,即“监视控制器”。更多内容请参见本文的 B 部分。该流程包括多个层,包括 UI、表示层、服务层、业务层、DAO 和数据层,使用数据传输对象 (DTO) 作为层之间的共享数据结构。此网站还在数据访问层和传输对象层使用 DLinq。
我解决的另一个问题是优化。所附的“Tests”解决方案测量了使用大多数在线参考文章中引用的典型模式实例化 DLinq Datacontext 时的加载时间。我在“工作单元”模式的约束下,为这种情况提出了一种优化方案。
系统要求
我使用了 2007 年 3 月 CTP Orcas 下载的 VPC 映像来开发所附的解决方案。在 SQL Server 2005 中,Microsoft 弃用了老旧的 Northwind 数据库,转而使用新的 Adventureworks 数据库。因此,我附上了我最喜欢的 Northwind 数据库的 2005 版本,这是我的代码运行所必需的。所附的“Tests”解决方案需要一些我已打包到 Northwind_custom_SPs.sql 脚本中的存储过程。
更新 - 原始 Orcas beta 1 版本“演示解决方案”的 Visual Studio 2008 beta 2 升级版现已可供下载。
关于性能方面的一些说明
大多数在线可用的 DLinq 示例都建议创建 DataContext 的新实例。这与 NHibernate 的 ISessionFactory 中使用的“工作单元模式”一致,该模式本质上是一种在对象生命周期内跟踪对象更改的机制。根据 Martin Fowler 的说法,实现“工作单元模式”的对象会在事务中跟踪其状态更改,并在最后以批处理方式触发 DML 查询以将其状态与数据库同步。DLinq 通过存在于 DataContext 实例范围内的更改跟踪服务来实现这一点。然而,在企业数据库的情况下,由 Sqlmetal 生成的映射源文件可能非常大。为每个工作单元创建 DataContext 的新实例可能会导致性能下降。我通过在上下文工厂中创建 Singleton MappingSource 实例来优化这种情况。每个工作单元 (DataContext) 都以调用 ContextFactory.CreateDataContext() 开始,工厂确保在 AppDomain 的生命周期内只读取一次映射源。
我感兴趣的另一个性能方面是 DML 转换。DLinq 使用 sp_executesql 将其状态更改转换为参数化 SQL 语句。默认情况下,DLinq ORM 映射将对象属性与表列关联。此默认映射导致生成 DML CRUD 语句(insert、update、delete、select)。或者,Orcas 设计器允许存储过程映射。这可以通过在设计器中右键单击实体并选择“配置行为”来在 DBML 文件中完成。有趣的是,DBML 设计器只允许您将 insert、update 和 delete 实体操作映射到自定义存储过程。
在默认 ORM 绑定下,DML 语句使用 sp_executesql 生成和执行。当定义了存储过程绑定时,映射的存储过程使用 sp_executesql 执行。现在,SQL Server 会缓存参数化查询的执行计划。附录列出了在 SQL profiler 中捕获到的生成查询。值得注意的是,生成的 DML 语句没有指定 DBO 架构。
随附的 Excel 文件包含两个工作表。第一个工作表比较了使用常规/典型方法与优化方法的加载时间。第二个工作表比较了使用两种 ORM 映射模式(实体到表和实体行为到存储过程)进行 CRUD 操作的执行时间。结果表明,后一种 ORM 映射方法(使用存储过程)性能更高。
设计与架构
我将首先概述使用三层架构并集成 DLinq 的网站,然后深入探讨另一个使用 MVP 模式和 DLinq 实现 N 层设计的网站。这两种架构都在数据层中集成了 DLinq。当本文的初稿发布时,我收到了一些读者关于我跨层使用 DLinq 实体的模式的询问。
我的架构的一个方面是将生成的 `DataContext` 和 DLinq 实体类分离到两个独立的层中。DLinq 在常规和 MVP 网站版本中都插入到数据层。关于构成调用堆栈的层的详细解释将在本文的 B 部分(深入探讨 MVP 网站)中给出。现在,只需说由 **SqlMetal** 生成的 DLinq 类在两个层中使用:`Northwind.Data`(数据访问层)和 `Northwind.Data.DTO`(传输对象层)。我已将映射文件以及实体类嵌入到传输对象程序集中。
其思想是 UI 层引用传输对象程序集(Northwind.Data.DTO.dll),并且不直接访问 Dlinq DataContext。DAL(Northwind.Data.dll)包含派生 DataContext、DAO 类和 ContextFactory。DAO 类充当网关,并直接引用 DataContext。在这两个网站中,构成传输对象层的 DLinq 实体跨层一直传递到 UI 层,在那里它们用于数据绑定。在下一节中,我将探讨这种方法的优缺点以及本文的范围。
如前所述,在性能部分,我创建了一个 `ContextFactory`,它位于数据访问层,并由数据访问对象 (DAO) 类调用。该工厂通过使用一个单例映射源来优化 `DataContext` 加载,该映射源通过从程序集的资源流中读取嵌入式映射文件资源来构建。`Northwind.Data.DTO` 程序集包含实体类以及嵌入式 ORM 映射资源文件。
static MappingSource CreateMappingSource()
{
string map = ConfigurationManager.AppSettings[Constants.MappingSourceKey];
if (String.IsNullOrEmpty(map)) map = Constants.DefaultMappingSource;
Assembly a = Assembly.Load(Constants.MappingAssembly);
Stream s = null;
if(a != null) s = a.GetManifestResourceStream(map);
if (s == null) s =
a.GetManifestResourceStream(Constants.DefaultMappingSource);
if(s == null)
throw new InvalidOperationException(
String.Format(@"The XML mapping file [{0}]
must be an embedded resource in the TransferObjects project.",
map));
return XmlMappingSource.FromStream(s);
}
SOA 和本文的范围 -
一些读者对我的架构在多层范例(如 SOA)中的实用性感兴趣。SOA 不在本文的讨论范围内。然而,本文中讨论的序列化、状态维护和乐观并发等问题也存在于 SOA 和具有非连接数据的多层环境中。我的设计是将 DLinq 实体打包到跨层共享的传输对象层中,这在 UI 和中间件层位于同一物理层时是理想的。在涉及对返回给 UI 的数据对象的访问控制限制,或者中间件(或 Web 服务)位于不同层的情况下,可以考虑替代方案,例如由装配器或纯 XML 构建的可序列化数据传输对象。
除了循环引用,DLinq 实体序列化在 SOA 场景中是支持的。VS 设计器允许生成用于序列化的 [DataContract] 类。XmlSerializer 也可以处理 Dlinq 类的序列化。然而,在撰写本文时,Dlinq 实体类没有标记 [Serializable],因此不能使用 BinaryFormatter 进行序列化。
A 部分:使用 DLinq 的典型三层架构
通用的三层架构由 UI、业务和数据层以及在各层之间移动的数据传输对象 (DTO) 组成。DTO 在 Java、Microsoft 和 Martin Fowler 的世界中可能具有不同的含义。在 Martin Fowler 的上下文中,DTO 与装配器协同工作,旨在减少客户端-服务器 RPC 的冗余。然而,我更倾向于 Sun 的“传输对象”模式,该模式本质上描述了在各层之间共享的数据结构。页面流在以下状态转换图中描述。

为了演示,我有两个版本的 CustomerDetails 页面。CustomerDetails1.aspx 包含一个绑定到 ObjectDatasource 控件的 FormView,用于选择和更新操作。Customer DTO 作为参数传递给 CustomerBO 中的更新方法。ObjectDatasource 控件的 DataObjectTypeName 属性指向 Customer 类型。另一方面,CustomerDetails2.aspx 覆盖了 FormView 的 ItemInserting、ItemUpdating 和 ModeChanging 事件,以调用业务对象 (CustomerBO) 上的方法并进行数据绑定。以下是更新的代码片段。
protected void fvCust_ItemUpdating(object sender, FormViewUpdateEventArgs e)
{
Customer c = Util.MarshalObject<Customer>(fvCust);
CustomerBO cbo = new CustomerBO(Util.CreateFactoryInstance());
if (c != null) cbo.UpdateCustomer(c);
SwitchMode(FormViewMode.ReadOnly);
}

序列图中的 IDataSource 表示绑定数据到 GridView 的 ObjectDataSource 控件。将 ObjectDataSource 的 TypeName 属性设置为 Northwind.Business.CustomerBO 不足以实例化 CustomerBO 类型,并且不允许默认构造函数。因此,代码隐藏覆盖了 ObjectDataSource 控件的 ObjectCreating 事件,以从应用程序配置文件中推断 DAOFactory 的类型,并将其作为参数传递给 CustomerBO 的重载构造函数。业务对象与 DAO(数据访问对象)通信。BaseBO(基本实体组件)持有一个对负责实例化 DAO 的 DAOFactory 的引用。上面的序列图详细描述了流程。
protected void CustomersDS_ObjectCreating(object sender,
ObjectDataSourceEventArgs e)
{
CustomerBO bo = new CustomerBO(Util.CreateFactoryInstance());
e.ObjectInstance = bo;
}
幕后:使用 DLinq 的 CRUD 操作
ORM 映射中描述的实体(在我的示例中由 SqlMetal.exe 生成)正在以传输对象模式使用。因此,它们被用作跨层的共享数据结构。这些映射到 Northwind 数据库模式的实体是使用以下命令生成的。
sqlmetal /server:. /database:northwind /map:c:\northwindMapping.xml
/pluralize /code:c:\NorthwindDTOs.cs
/language:csharp /namespace:Northwind.Data.DTO
分页和排序
使用 ObjectDatasource 进行分页和排序就像在 GridView 上将 AllowPaging 和 AllowSorting 设置为 true,并配置 ObjectDatasource 的 SelectCountMethod、SortParameterName、MaximumRowsParameterName 和 EnablePaging 属性一样简单。CustomerBO 的 GetCustomersByPage 方法(在 ObjectDatasource 的 TypeName 和 SelectMethod 属性中配置)调用 DAO 上的相应方法。有关详细信息,请参阅随附的“Web”项目中的 CustomerListing.aspx。业务对象 (BO) 通过 DAOFactory 使用抽象工厂模式获取对相应 DAO 的引用。
在调用堆栈中,业务层到数据层之间的控制流在常规网站版本和 MVP 网站版本中保持相同。不同之处在于它如何到达业务层。更多内容请参见本文的 B 部分。绑定到 ObjectDatasource 控件的 GridView 在用户单击列标题时会自动翻转排序表达式。DAO 将其转换为 DLinq 查询,这些查询返回一个可枚举的 DTO 对象集,如以下代码片段所示。
public IEnumerable <Customer> GetCustomersByPage(int startRowIndex,
int pageSize, string sortParam)
{
NorthwindContext c = ContextFactory.CreateDataContext();
IOrderedQueryable<Customer> qry = null;
switch (sortParam)
{
case "ContactName DESC":
qry = c.Customers.OrderByDescending(cust => cust.ContactName);
break;
case "ContactName":
qry = c.Customers.OrderBy(cust => cust.ContactName);
break;
case "ContactTitle DESC":
qry = c.Customers.OrderByDescending(cust => cust.ContactTitle);
break;
case "ContactTitle":
qry = c.Customers.OrderBy(cust => cust.ContactTitle);
break;
case "CompanyName DESC":
qry = c.Customers.OrderByDescending(cust => cust.CompanyName);
break;
case "CompanyName":
qry = c.Customers.OrderBy(cust => cust.CompanyName);
break;
case "Country DESC":
qry = c.Customers.OrderByDescending(cust => cust.Country);
break;
case "Country":
qry = c.Customers.OrderBy(cust => cust.Country);
break;
case "PostalCode DESC":
qry = c.Customers.OrderByDescending(cust => cust.PostalCode);
break;
case "PostalCode":
qry = c.Customers.OrderBy(cust => cust.PostalCode);
break;
case "City DESC":
qry = c.Customers.OrderByDescending(cust => cust.City);
break;
case "City":
qry = c.Customers.OrderBy(cust => cust.City);
break;
}
return qry == null
? c.Customers.Skip(startRowIndex).Take(pageSize)
: qry.Skip(startRowIndex).Take(pageSize);
}
以下 LINQ 查询生成客户最近五个订单的商品数量、花费金额及其他详细信息报告。
var dql = (from in ctx.Orders
Where o.CustomerID == CustomerId
join od in ctx.OrderDetails on o.OrderID equals od.OrderID
orderby o.OrderDate descending select new{
OrderID = o.OrderID,
OrderItems = o.OrderDetails.Count,
AmountSpent =
o.OrderDetails.Sum(ord => ord.UnitPrice * ord.Quantity),
ProductId = od.ProductID,
ShipCity = o.ShipCity,
ShipCountry = o.ShipCountry
}
).Take(5);
在本文中,我不会深入探讨 DLinq 表达式(抽象语法树)到 SQL 查询转换的细节。如前所述,我假设读者熟悉 DLinq 基础知识。关于 SQL 转换和其他 DLinq 方面的重要资源可以在 MSDN 上找到。
插入和更新操作
如前面状态转换图中描述的,客户详细信息页面有两种实现。CustomerDetails1.aspx 使用 RAD 方法,通过将 FormView 绑定到 ObjectDatasource。
<asp:ObjectDataSource ID="CustomerDS" runat="server"
SelectMethod="GetCustomer"
TypeName="Northwind.Business.CustomerBO"
DataObjectTypeName="Northwind.Data.DTO.Customer"
UpdateMethod="UpdateCustomer" InsertMethod="InsertCustomer"
onobjectcreating="CustomerDS_ObjectCreating"
ConflictDetection="CompareAllValues"
OldValuesParameterFormatString="cachedCustData"
>
<selectparameters>
<asp:querystringparameter DefaultValue="0"
Name="id" QueryStringField="custid"
Type="String" />
</selectparameters>
</asp:ObjectDataSource>
同一个接口实现了对客户的 `create`、`update` 和 `read` 操作。在 `CustomerDetails2.aspx` 中,我使用了一种替代的手动实现,因为 `ObjectDatasource` 并非适用于所有情况的企业级应用程序工具。在这两种页面流中,`CustomerDAO` 将 `Customer` DTO 作为写入操作的参数,并将客户 ID 作为读取操作的参数。
`CustomerDAO` 公开了一个 `GetCustomerEnumerable` 方法,该方法返回 `IEnumerable
public Customer GetCustomer(string id)
{
NorthwindContext ctx = ContextFactory.CreateDataContext();
return (from c in ctx.Customers where c.CustomerID == id select c)
.SingleOrDefault();
}
public IEnumerable <Customer> GetCustomerEnumerable(string id)
{
NorthwindContext ctx = ContextFactory.CreateDataContext();
return (from c in ctx.Customers where c.CustomerID == id select c)
.ToList();
}
以下代码插入一个新客户。
public string InsertCustomer(Customer cust)
{
if (cust == null) return null;
NorthwindContext c = ContextFactory.CreateDataContext();
c.Customers.Add(cust);
c.SubmitChanges();
return cust.CustomerID;
}
更新(和删除)操作的并发模式
DLinq 中更改跟踪机制的最大优势在于它能够推断出脏数据,并且只向更改的字段发出更新,从而省去了在领域对象中进行经典的 `IsDirty` 检查的需要。在客户端-服务器架构中典型的非连接数据操作中,更新操作需要更多的工作。这是因为与新 `DataContext` 关联的 DLinq 的身份缓存是空的。
DLinq 支持多种方式执行写入操作,有或没有乐观并发。Microsoft 的 Keith Farmer 在以下讨论中总结了 API。我将简要介绍数据库编程中常用的几种典型乐观并发模式。DLinq 顺便也支持这些模式。
- 检查所有列或部分列(相对于快照)——默认情况下,所有列的 UpdateCheck 都已开启,这导致它们参与并发冲突检测。或者,只有少数感兴趣的列可以通过在映射文件中为其他列指定 UpdateCheck="Never" 来标记为并发检查。这导致它们从生成的 SQL 的 where 子句中排除。
- 时间戳 — 可以使用时间戳代替 UpdateCheck 进行乐观并发检查。使用时间戳有利有弊,例如修改没有时间戳列的表的设计,以及在写入操作后检索时间戳新值的额外 select 语句开销(尽管由于在同一调用中进行了批处理,开销很小)。
- 一种有趣的冲突检测模式是只检查用户修改过的那些列的原始值。
update customers set name=@name where id=@id and name=@original_name
DLinq 通过 `UpdateCheck="WhenChanged"` 列属性定义支持此模式。
我倾向于认为 #3 是三种模式中最“乐观”的。#1(所有列)和 #2(时间戳)的粒度是整个行。如果一个会话修改了列 A,而另一个会话在第一个会话提交更改之前修改了列 B,则在模式 #1 和 #2 中会发生乐观并发冲突。然而,模式 #3 允许两个更新都发生,因为在这种情况下,会话 1 和 2 之间没有真正的冲突。模式 #3 在数据传输方面更高效,因为只有当列被当前会话修改时,它才包含在 where 子句中。但是,如果当前会话没有修改列,意图使其保持不变,而另一个会话在此期间修改了其数据库值,则不会检测到并发冲突。
除了 `UpdateCheck` 元数据,还可以通过如下方式在 `DataContext` 上指定 `RefreshMode` 来主动定义并发解决行为。这定义了在提交操作查询之前的覆盖行为。新的 `DataContext` 无法从非连接的 `custData` 实例中确定数据库的原始状态。调用 `DataContext.Refresh` 会拉取最新的数据库快照并将其与 `custData` 实例合并,从而刷新该实例。
public void UpdateCustomer(Customer custData)
{
if (custData == null) return;
NorthwindContext c = ContextFactory.CreateDataContext();
c.Customers.Attach(custData);
// The foll stmt retrieves the latest database snapshot with a select
// statement
c.Refresh(RefreshMode.KeepCurrentValues, custData);
// This stmt does an update on only the changed fields
c.SubmitChanges();
}
与上述无状态场景不同,在 Web 环境中维护旧状态很容易。我演示了两种维护旧状态的方式——`CustomerDetails1.aspx` 在客户端维护先前状态,而 `CustomerDetails2.aspx` 在服务器端用户会话范围内存储它。当 `ConflictDetection="CompareAllValues"` 时,`ObjectDataSourcee` 会自动维护客户端快照。
<asp:ObjectDataSource ID="CustomerDS" runat="server"
TypeName="Northwind.Business.CustomerBO"
DataObjectTypeName="Northwind.Data.DTO.Customer"
UpdateMethod="UpdateCustomer"
ConflictDetection="CompareAllValues"
OldValuesParameterFormatString="cachedCustData"
对 GridView 的更新会调用 CustomerBO 上的 `UpdateCustomer` 方法。`OldValuesParameterFormatString="cachedCustData"` 属性设置会自动调用 CustomerBO 上接受名为“`cachedCustData`”的旧值参数的相应方法。
`CustomerDetails` 实现的两个变体都在调用堆栈的更高级别调用 `CustomerDAO` 中的 `UpdateCustomer` 方法,该方法接受两个 `Customer` 类型的参数。将旧快照与当前实例进行比较,只有已修改的属性才会进入更新 SQL 语句的 `set` 子句。参与更新检查的旧快照中的属性在 `where` 子句中设置。当检测到并发冲突时,`RefreshMode.KeepCurrentValues` 会向 DLinq 指示应保留当前会话所做的最新更改。旧快照创建后,一个或多个会话所做的任何更改都将回滚。
public void UpdateCustomer(Customer custData, Customer cachedCustData)
{
if (custData == null) return;
NorthwindContext c = ContextFactory.CreateDataContext();
c.Customers.Attach(custData, cachedCustData);
try {
c.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException) {
c.ChangeConflicts.ResolveAll(RefreshMode.KeepCurrentValues);
c.SubmitChanges();
}
}
B 部分:使用 DLinq 的 N 层 MVP 架构
模型-视图-呈现器(MVP)模式是一种在应用程序中松散耦合层之间交互的技术。该技术基于 setter 和构造函数实现的依赖注入,使得代码可以独立测试,不受运行时宿主环境的依赖或影响。有关 MVP 的更多信息可以在这篇 MSDN 文章中找到。
Martin Fowler 将 MVP 分为子模式,即“监视控制器”、“被动视图”和“演示模型”。我的网站中使用的 MVP 风格倾向于“监视控制器”变体。此模式利用了数据绑定和 ASP.NET 框架的视图实现基本基础设施支持。任何非平凡的 UI 逻辑都由呈现器处理。在此模式中,与“被动视图”变体相反,视图了解领域对象,数据绑定引入了从视图到领域层的定向依赖。另一点值得注意的是视图的可测试性。此模式忽略了视图到模型数据绑定和视图同步的测试。它强调只测试呈现器中更复杂的 UI 逻辑。在我看来,这是两全其美的方法,因为我们可以安全地避免测试 ASP.NET 基础设施,而是更有效地利用我们的时间!
MVP-DLinq 网站的架构

尽管其余部分从图中显而易见,但值得关注的是呈现器和业务程序集。为了允许对呈现器、服务和业务层进行单元测试,一种典型的依赖注入模式是“分离接口模式”。呈现器程序集暴露在 Web 程序集中实现的视图接口(视图接口可以被模拟或存根,用于单元测试呈现器)。呈现器通过视图接口与视图实现进行通信。同样,业务层定义了一组数据接口,并通过这些接口与数据层进行通信。这也是“网关模式”的一个示例,因为它涉及外部资源。因此,有趣的是,这成为“控制反转”的一个案例,其中数据访问层引用业务层并实现业务层中定义的数据接口。
流程和调用堆栈

视图初始化器和视图

我为视图和呈现器之间的初始化行为设计了一个稍微复杂但可重用的模式。`BasePage`(如上所示)是一个抽象的视图初始化器。它包含一个 `DAOFactory` 属性,该属性返回一个使用配置元数据创建的具体 `IDAOFactory` 实例。
public static IDaoFactory CreateFactoryInstance()
{
string typeStr =
ConfigurationManager.AppSettings[Constants.DAO_FACTORY_KEY];
if (String.IsNullOrEmpty(typeStr)) typeStr =
Constants.DEFAULT_FACTORY_TYPE;
IDaoFactory f = null;
try
{
Assembly a = Assembly.Load(Constants.FACTORY_ASSEMBLY);
if (a != null)
{
Type fType = a.GetType(typeStr);
if (fType == null) fType = a.GetType(String.Format("{0}.{1}",
Constants.FACTORY_ASSEMBLY, typeStr));
f = Activator.CreateInstance(fType) as IDaoFactory;
}
}
catch (Exception ex)
{
throw new Exception(String.Format(
"Unable to load {0} from Northwind.Data", typeStr), ex);
}
return f;
}
任何派生视图初始化器都必须实现两个抽象方法:`CreatePresenters()` 和 `InitializeViews()`。这两个方法在页面生命周期的 `Init` 阶段被调用。我的演示网站中的 `CustomerListing.aspx` 既充当视图,又充当视图初始化器。它创建 `ListCustomersPresenter` 并通过将委托与呈现器中的方法连接起来,初始化视图上的小部件。您可能已经注意到,我已将委托实现为 `InitializeViews()` 方法中的匿名方法。
protected override void CreatePresenters()
{
_presenter = new ListCustomersPresenter(this, base.DaoFactory);
}
protected override void InitializeViews()
{
CommandEventHandler hnd = delegate(object sender, CommandEventArgs e)
{
int curr = this.CurrentPageIndex;
PageIndexChanged = true;
switch (e.CommandName)
{
case "first":
this.CurrentPageIndex = 0;
break;
case "prev":
CurrentPageIndex = (--curr < 0) ? (0) : curr;
break;
case "next":
CurrentPageIndex = (++curr == TotalPages) ? (curr - 1) : curr;
break;
case "last":
this.CurrentPageIndex = TotalPages - 1;
break;
}
_presenter.ShowCurrentPage();
};
imgF.Command += hnd;
imgL.Command += hnd;
imgN.Command += hnd;
imgP.Command += hnd;
grd.PageIndexChanging += delegate(object sender, GridViewPageEventArgs e)
{
this.CurrentPageIndex = e.NewPageIndex;
_presenter.ShowCurrentPage();
};
grd.Sorting += delegate(object sender, GridViewSortEventArgs e)
{
this.SortPropertyName = e.SortExpression;
_presenter.ShowCurrentPage();
};
}
视图-呈现器初始化模式
当在视图初始化器的 `CreatePresenters` 实现中实例化呈现器时,内部方法调用链可确保呈现器和视图彼此之间持有强类型引用。我设计的视图-呈现器初始化模式通过 `BasePage`、`BasePresenter`、`View` 和泛型参数之间的一系列方法调用确保了这种初始化顺序。以下详细说明了调用序列。`BasePage` 视图初始化器的 `OnLoad` 覆盖如下。
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Initialize each of the views on the page for GET
if(!IsPostBack) foreach (IBaseView view in Views) view.InitializeView();
}
抽象的 "Views" 属性是我设计的一个有趣部分。任何派生的视图初始化器都实现此属性以返回其包含的 `View` 列表。通常,一个视图初始化器可能包含一个或多个视图(实现为用户控件小部件)和一个或多个呈现器。由于 `CustomerListing.aspx` 本身就是一个视图,因此 "Views" 属性实现返回一个 `List
public void InitializeView()
{
CurrentPageIndex = 0;
Presenter.InitializeView();
}
由于 CustomerListing 也是一个视图,所以“Presenter”指的是 IView
中定义的抽象 Presenter 属性。视图接口层次结构定义如下。

public interfaceIBaseView
{
// View implementation may call Presenter.InitailizeView()
void InitializeView();
}
public interfaceIView<P, DTO> : IBaseView
where P : BasePresenter<P, DTO>
{
P Presenter { get; set; }
bool PageIndexChanged { get; set; }
// Bind data source to view controls
void DisplayData(IEnumerable <DTO> dataSource); // collection
void DisplayData(DTO dataSource); // single item
}
如上图所示,`IViewCustomerListing` 继承自 `IView
public interfaceIViewCustomerListing : IView<ListCustomersPresenter,
Customer>
有趣的是,`BasePresenter` 的定义带有一个指向派生呈现器的模板参数!这种不寻常的设计是模板转换规则的结果,其中对于封闭类型没有隐式或显式转换,尽管模板参数可以在层次结构中隐式转换。例如,`List
`BasePresenter` 的 `InitializeView` 实现如下。这负责在 `GET` 请求时自动初始化由呈现器处理的视图。如果您还记得,对 `InitializeView()` 的调用是由抽象的 `BasePage` 视图初始化器为 `GET` 请求发起的。
/*
* Template (polymorphic) method that
* standardises presenter->view initialization
*/
public virtual void InitializeView()
{
// GetInitialViewData is an abstract method that is
// implemented in a concrete presenter
// to fetch the initial data for databinding on the GET
// request for a page
IEnumerable<DTO> dataSource = GetInitialViewData();
if (dataSource != null ) View.DisplayData(dataSource);
}
当在视图初始化器的 `CreatePresenters` 方法中实例化呈现器时,`BasePresenter` 实现可确保视图和呈现器获得对彼此的强类型引用。
public BasePresenter(IView<P, DTO> view, IDaoFactory daoFactory)
{
View = view;
_daoFactory = daoFactory;
}
`BasePresenter` 的 `View` setter 将底层视图的 presenter 初始化为其自身。这里 `BasePresenter
` 被转换为 `P`,以便视图持有对派生 presenter 的强类型引用。
protected virtual IView<P, DTO> View
{
get { return _view; }
set
{
_view = value;
_view.Presenter = (P) this;
}
}
ListCustomersPresenter(BasePresenter 的派生实现)覆盖了 BasePresenter 的 View,以方便创建对 IViewCustomerListing 的强类型引用。
/*
* Override base implementation in case the presenter
* needs strongly typed access
* to the specific view
*/
protected new IViewCustomerListing View
{
get { return (IViewCustomerListing)base.View; }
set { base.View = (IView<ListCustomersPresenter, Customer>)value; }
}
业务层封装业务规则

业务层在 MVP 中的作用值得商榷。为什么要在服务层之上设置业务层?在我的示例网站中,我将业务层用作服务层的网关。这是因为我没有在示例中引入业务规则,以使其足够简单以供演示。在我看来,业务层可以封装与实体对应的业务规则。在企业应用程序中进行业务规则处理的一种方式是,呈现器访问业务对象进行规则处理,而服务对象将是数据访问和操作的网关。业务规则处理的另一种模式是使用责任链模式实现规则处理引擎。
业务规则违规可以通过以下模式处理。业务或规则对象通知呈现器业务规则违规,呈现器相应地将视图的属性设置为无效状态。视图对属性的实现设置验证器控件的“IsValid”属性,从而通知用户业务规则异常。这属于个人意见,但我会倾向于这样一种设计:由呈现器负责检查业务规则,并且服务层不依赖于业务或规则对象。我会认为呈现器的任务是使用规则处理器进行业务规则检查,而服务层主要充当使用数据层访问数据的网关。
用于单元测试业务/服务层的抽象工厂模式

通常,业务/服务层在网关模式中使用 DAO 进行数据访问。抽象工厂模式用于通过在业务对象中注入模拟或存根作为网关依赖项来隔离单元测试业务/服务层。我的业务对象设计使用基于构造函数的依赖注入而不是使用 setter。
public CustomerBO(IDaoFactory daoFactory) : base(daoFactory)
{
_dao = base.daoFactory.CreateCustomerDAO();
}
以下是 `CustomerBO` 类的 `GetAllCustomers()` 方法的单元测试示例(使用 Rhino Mocks)。
[Test]
public void TestCustomerBO()
{
MockRepository rep = new MockRepository();
ICustomerDAO mockCustDAO = rep.CreateMock<ICustomerDAO>();
IDaoFactory mockDAOFactory = rep.CreateMock<IDaoFactory>();
List<Customer> stubList = new List<Customer>();
using (rep.Record())
{
Expect.Call(daoFactory.CreateCustomerDAO()).Return(mockCustDAO);
Expect.Call(mockCustDAO.GetAllCustomers()).Return(stubList);
}
using (_rep.Playback())
{
CustomerBO c = new CustomerBO(mockDAOFactory);
IEnumerable<Customer> custList = c.GetAllCustomers();
}
Assert.IsNotNull(custList);
}
传输对象层

我已将从 SqlMetal 生成的实体类打包到 `Northwind.Data.DTO` 命名空间中。
结论
我讨论了在企业应用程序场景中使用 DLinq 时架构、设计和性能的一些方面。这绝不是详尽无遗的,还有其他方法可以实现相同的最终结果。如果您正在寻找更多 DLinq 性能提示,Rico Mariani 的博客是一个极好的资源。尽管本文是关于 DLinq 的,但值得一提的是,其竞争对手包括 NHibernate 和 ADO.NET 实体框架。目前,LINQ 仍然是 CTP,不像 NHibernate。在我看来,Visual Studio Orcas 内置的 LINQ 支持和查询优化使得在不久的将来使用 DLinq 而非 NHibernate 具有令人信服的理由。Rhino Mocks 的开发者 Oren Eini 发布了 NHibernate 版 LINQ 的 Alpha 版本,旨在让 NHibernate 重新回到人们的视野中。另一个值得注意的有趣框架是 LINQ to Entities,也称为 ADO.NET 实体框架。该框架在实体对象模型和底层关系数据库之间提供了一层额外的抽象,使其与数据库的耦合度比 DLinq 更松散。