高级内容
本指南的目的是介绍自定义实体与集合的概念及使用。使用自定义实体是业界广泛采用的做法,因此,也就产生了同样多的模式以处理各种情况。设计模式具有优势的原因有很多。首先,在处理具体的情况时,您可能不是第一次碰到某个给定的问题。设计模式使您可以重新使用给定问题的已经过尝试和测试的解决方案(虽然设计模式并不意味着全盘照抄,但它们几乎总是能够为解决方案提供一个可靠的基础)。相应地,这使您对系统随着复杂性增加而进行缩放的能力充满了信心,不仅因为它是一个广泛使用的方法,还因为它具有详尽的记录。设计模式还为您提供了一个通用的词汇表,使知识的传播和传授更容易实现。
不能说设计模式只适用于自定义实体,实际上许多设计模式都并非如此。但是,如果您找机会试一下,您可能会惊喜地发现许多记载详尽的模式确实适用于自定义实体和映射过程。
最后这一部分专门介绍大型或较复杂的系统可能会碰到的一些高级情况。因为大多数主题都可能值得您单独学习,所以我会尽量为您提供一些入门资料。
Martin Fowler 的 Patterns of Enterprise Application Architecture 就是一个很好的入门材料,它不仅可以作为常见设计模式的优秀参考(具有详细的解释和大量的示例代码),而且它的前 100 页确实可以让您透彻地了解整个概念。另外,Fowler 还提供了一个联机模式目录,它对于已经熟悉概念但需要一个便利参考的人士很有用。
并发
前面的示例介绍的都是从数据库中提取数据并根据这些数据创建对象。总体而言,更新、删除和插入数据等操作是很直观的。我们的业务层负责创建对象、将对象传递给数据访问层,然后让数据访问层处理对象世界与关系世界之间的映射。例如:
'Visual Basic .NET
Public sub UpdateUser(ByVal user As User)
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As New SqlCommand("UpdateUser", connection)
' 可以借助可重新使用的函数对此进行反向映射
command.Parameters.Add("@UserId", SqlDbType.Int)
command.Parameters(0).Value = user.UserId
command.Parameters.Add("@Password", SqlDbType.VarChar, 64)
command.Parameters(1).Value = user.Password
command.Parameters.Add("@UserName", SqlDbType.VarChar, 128)
command.Parameters(2).Value = user.UserName
Try
connection.Open()
command.ExecuteNonQuery()
Finally
connection.Dispose()
command.Dispose()
End Try
End Sub
//C#
public void UpdateUser(User user) {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command = new SqlCommand("UpdateUser", connection);
// 可以借助可重新使用的函数对此进行反向映射
command.Parameters.Add("@UserId", SqlDbType.Int);
command.Parameters[0].Value = user.UserId;
command.Parameters.Add("@Password", SqlDbType.VarChar, 64);
command.Parameters[1].Value = user.Password;
command.Parameters.Add("@UserName", SqlDbType.VarChar, 128);
command.Parameters[2].Value = user.UserName;
try{
connection.Open();
command.ExecuteNonQuery();
}finally{
connection.Dispose();
command.Dispose();
}
}
但在处理并发时就不那么直观了,也就是说,当两个用户试图同时更新相同的数据时会出现什么情况呢?默认的行为(如果您没有执行任何操作)是最后提交数据的人将覆盖以前所有的工作。这可能不是理想的情况,因为一个用户的工作将在未获得任何提示的情况下被覆盖。要完全避免所有冲突,一种方法就是使用消极的并发技术;但此方法需要具有某种锁定机制,这可能很难通过可缩放的方式实现。替代方法就是使用积极的并发技术。让第一个提交的用户控制并通知后面的用户是通常采取的更温和、更用户友好的方法。这可以通过某种行版本控制(例如时间戳)来实现。
参考资料:
• Introduction to Data Concurrency in ADO.NET
• CSLA.NET's concurrency techniques
• Unit of Work design pattern
• Optimistic offline lock design pattern
• Pessimistic offline lock design pattern
性能
与合理的灵活性和功能问题相对的是,我们经常担心细小的性能差异。尽管性能的确很重要,但提供适用于一切情况而不是最简单情况的通用原则通常很难。例如,将自定义集合与 DataSet 相比,哪个更快?使用自定义集合,您可以大量使用 DataReader,这是从数据库中提取数据的较快方式。但答案实际上取决于您使用它们的方式以及处理的数据类型,所以一般性的说明没有任何用。更重要的一点是要认识到,不管您能节省多少处理时间,与维护性方面的差异相比都可能微不足道。
当然,并不是说您不可能找到一个既具有高性能又可维护的解决方案。虽然我强调说答案实际上取决于您的使用方式,但的确有一些模式可以帮助您最大程度地提高性能。但是,首先要知道的是自定义实体与集合缓存以及 DataSet,并且能够利用相同的机制(类似于 HttpCache)。DataSet 的优势之一是它能够编写 Select 语句,以便只获取所需的信息。使用自定义实体时,您常常感到不得不填充整个实体以及子实体。例如,如果要通过 DataSet 显示一个 Organization 列表,您可以只提取 OganizationId、Name 和 Address 并将其绑定到重复器。使用自定义实体时,我总觉得还需要获取所有其他的 Organization 信息,如果该组织通过了 ISO 认证,则可能是一个位标记,即所有员工、其他联系信息等的集合。可能其他人没有碰到这个大难题,但幸运的是,如果我们愿意,我们可以对自定义实体进行很好的控制。最常用的方法是使用一种延迟加载模式,它只在首次需要时获取信息(可以很好地封装在属性中)。这种对各个属性的控制提供了通过其他方式无法轻易获得的巨大灵活性(请想象一下在 DataColumn 级别执行类似操作的情况)。
参考资料:
• Lazy Load 设计模式
• CSLA.NET lazy load
排序与筛选
虽然 DataView 对排序和筛选的内置支持需要您了解有关 SQL 和基础数据结构的知识,但它提供的方便确实是自定义集合所不具备的。我们仍然可以排序和筛选,但首先需要编写功能。因为技术不一定是最先进的,所以代码的完整描述不属于本节要讨论的范围。大多数技术都很相似,例如使用筛选器类筛选集合以及使用比较器类进行排序,我认为不存在固定的模式。但是,的确存在一些参考资料:
• Generic sort function
• Sorting & Filtering Custom Collections 教程
代码生成
解决概念上的障碍后,自定义实体与集合的主要缺点就是灵活性、抽象和维护性差所导致的代码数量的增加。实际上,您可能会认为我所说的维护成本和错误的降低这一切都抵不上代码的增加。虽然这一观点是成立的(同样,因为任何解决方案都不是完美无缺的),但可以通过设计模式和框架(例如 CSLA.NET)大大缓解此问题。代码生成工具与模式和框架完全不同,这些工具可以大大降低您实际需要编写的代码数量。本指南最初打算专门辟出一节详细介绍代码生成工具,特别是流行的免费 CodeSmith;但现有的许多参考资料都可能超出了我自己对该产品的认识。
在继续之前,我认识到代码生成听起来像天方夜谭一样。但经过正确的使用和理解后,它的确是您工具包中不可缺少的一个强大的武器,即使您没有处理自定义实体也是如此。虽然代码生成的确不仅仅适用于自定义实体,但很多都是专为自定义实体而设计的。原因很简单:自定义实体需要大量重复代码。
简言之,代码生成是如何工作的?构想听起来好像遥不可及甚至反而会降低效率,但您基本上通过编写代码(模板)来生成代码。例如,CodeSmith 附带了许多强大的类,使您可以连接到数据库并获取所有属性:表、列(类型、大小等)和关系。获得这些信息后,我们前面讨论的大部分工作都可以自动完成。例如,开发人员可以选择一个表,然后使用正确的模板自动创建自定义实体(带有正确的字段、属性和构造函数),并获得映射函数、自定义集合以及基本的选择、插入、更新和删除功能。甚至还可以更进一步,实现排序、筛选以及我们提到的其他高级功能。
CodeSmith 还附带了许多现成的模板,可以作为很好的学习资料。最后,CodeSmith 还为实现 CSLA.NET 框架提供了许多模板。我最初只花了几个小时来学习基本概念、熟悉 CodeSmith 的功能,但它为我节省的时间已经多得无法计算了。另外,如果所有的开发人员都使用相同的模板,代码的高度一致性将使您能够轻松地继续其他人的工作。
参考资料:
• Code Generation with CodeSmith
• CodeSmith 主页
O/R 映射器
即使因为对 O/R 映射器知之甚少使我不敢随便对它们发表议论,但它们自身的潜在价值使其不容忽视。代码生成器生成基于模板的代码,供您复制并粘贴到您自己的源代码中,而 O/R 映射器则在运行时通过某种配置机制动态生成代码。例如,在 XML 文件中,您可以指定某个表的列 X 映射到某个实体的属性 Y。您仍然需要创建自定义实体,但是集合、映射和其他数据访问函数(包括存储过程)都是动态创建的。从理论上讲,O/R 映射器几乎可以完全解决自定义实体存在的问题。随着关系世界和对象世界的差异越来越明显以及映射过程越来越复杂,O/R 映射器的价值就变得越发不可限量了。O/R 映射器的两个缺点据说就是不够安全和性能较差(至少在 .NET 环境中是这样)。根据我所阅读的资料,我确信它们并不是不够安全,虽然在有些情况下性能较差,但在另外一些情况下却表现突出。O/R 映射器并不适合所有情况,但如果您要处理复杂的系统,则应尝试一下它们的功能
参考资料:
• Mapper 设计模式
• Data Mapper 设计模式
• Wilson ORMapper
• Frans Bouma 关于 O/R 映射的帖子
• LLBGenPro
• NHibernate
.NET Framework 2.0 的功能
即将面世的 .NET Framework 2.0 版将改变我们在本指南中讨论的一些实施细节。这些改变将减少支持自定义实体所需的代码数量,并有助于处理映射问题。
泛型
议论颇多的泛型之所以存在,主要原因之一就是为了向开发人员提供现成的强类型的集合。我们避开 Arraylist 等现有集合是因为它们属于弱类型。泛型提供了与当前集合同样的方便性,而且它们属于强类型。这是通过在声明时指定类型来实现的。例如,我们可以替换 UserCollection 而不需要增加代码,然后只需创建一个 List 泛型的新实例并指定我们的 User 类即可:
'Visual Basic .NET
Dim users as new IList(of User)
//C#
IList users = new IList();
声明后,我们的 user 集合就只能处理 User 类型的对象了,这为我们提供了编译时检查和优化的所有优点。
参考资料:
• Introducing .NET Generics
• An Introduction to C# Generics
可以为空的类型
可以为空的类型实际上就是由于其他原因而非上述原因而使用的泛型。处理数据库时面临的挑战之一就是正确一致地处理支持 NULL 的列。在处理字符串和其他类(称为引用类型)时,您只需为代码中的某个变量指定 nothing/null:
'Visual Basic .NET
if dr("UserName") Is DBNull.Value Then
user.UserName = nothing
End If
//C#
if (dr["UserName"] == DBNull.Value){
user.UserName = null;
}
也可以什么都不做(默认情况下,引用类型为 nothing/null)。这对值类型(例如整数、布尔值、小数等)并不完全一样。您当然也可以为这些值指定 nothing/null,但这样将会指定一个默认值。如果您只声明整数,或者为其指定 nothing/null,变量的值实际上将为 0。这使其很难映射回数据库:值究竟为 0 还是 null?可以为空的类型允许值类型具有具体的值或者为空,从而解决了这个问题。例如,如果我们要在 userId 列中支持 null 值(并不是很符合实际情况),我们会首先将 userId 字段和对应的属性声明为可以为空的类型:
'Visual Basic .NET
Private _userId As Nullable(Of Integer)
Public Property UserId() As Nullable(Of Integer)
Get
Return _userId
End Get
Set(ByVal value As Nullable(Of Integer))
_userId = value
End Set
End Property
//C#
private Nullable userId;
public Nullable UserId {
get { return userId; }
set { userId = value; }
}
然后利用 HasValue 属性判断是否指定了 nothing/null:
'Visual Basic .NET
If UserId.HasValue Then
Return UserId.Value
Else
Return DBNull.Value
End If
//C#
if (UserId.HasValue) {
return UserId.Value;
} else {
return DBNull.Value;
}
参考资料:
• Nullable types in C#
• Nullable types in VB.NET
迭代程序
我们前面讨论的 UserCollection 示例只展示了自定义集合中可能需要的基本功能。有一个操作无法通过所提供的实现来完成,即通过一个 foreach 循环在集合中循环。要完成此操作,您的自定义集合必须具有实现 IEnumerable 接口的枚举数支持类。这是一个非常直观且重复性较强的过程,但却引入了更多的代码。C# 2.0 引入了新的 yield 关键字来为您处理此接口的实现细节。Visual Basic .NET 中当前没有与新的 yield 关键字等效的关键字。
参考资料:
• What's new In C# 2.0 - Iterators
• C# Iterators
小结
请勿轻率地做出向自定义实体与集合转换的决定。这里有许多需要考虑的因素。例如,您对 OO 概念的熟悉程度、可用来熟悉新方法的时间以及您打算部署它的环境。虽然总体上它们有很大的优点,但并不一定适合您的特定情况。即使适合您的情况,它们的缺点也可能会打消您使用它们的念头。还要记住有许多可替代的解决方案。Jimmy Nilsson 在他的 Choosing Data Containers for .NET 中概述了其中的某些替代方案,此专栏系列包括 5 部分(1、2、3、4、5)。
自定义实体使您获得了面向对象的编程的丰富功能,并帮助您构建了可靠、可维护的 N 层体系结构的框架。本指南的目的之一是让您从构成系统的业务实体,而不是一般的 DataSet 和 DataTable 的角度来考虑您的系统。我们还讨论了一些关键的问题,不管您选择的途径(即设计模式)、对象世界与关系世界的差异(了解详细信息)以及 N 层体系结构是什么,您都应注意这些问题。请记住,您之前花费的时间会在系统的整个生命周期内为您带来更多的回报。
相关书籍
• Microsoft ASP.NET Coding Strategies with the Microsoft ASP.NET Team
• Expert C# Business Objects
• Expert One-on-One Visual Basic .NET Business Objects