规则4:仔细选择外部接口
当您设计数据访问类的方法时,需要考虑它们将如何接收和返回数据。对于大多数开发人员而言,有三种主要选择:直接使用ADO.NET对象、使用XML和使用自定义类。如果您要直接公开ADO.NET对象,则可以利用两个编程模型中的一个。第一个模型包含DataSet和DataTable对象,它们对于断开连接的数据访问很有用。关于DataSet及其关联的DataTable,已在许多文章中进行过介绍,但是在需要处理已经与基础数据存储区断开连接的数据时,它们非常有用。换句话说,DataSet可以在应用层之间传递,即使这些层分布在不同的物理位置也可以进行传递,如在业务层和数据服务层被放在与表示服务不同的服务器群集上的情况下。
另外,对于通过基于XML的Web Service返回数据的情况来说,DataSet对象也是理想的选择,这是因为它们可以序列化,因而可以在SOAP响应消息中返回。这有别于使用实现IDataReader接口的类(例如,SqlDataReader和OleDbDataReader)访问数据。这些数据读取器用于以只进、只读方式访问数据。这两者之间的巨大差异在于:DataSet和DataTable对象可以按值在应用程序域之间(因而也可以在相同或不同计算机上的进程之间)传递,而数据读取器则可以到处传递,并且总是按引用传递。请参见图5,其中Read和GetValues是在服务器进程中执行的,并且它们的返回值被复制到客户端。
图5 远程处理数据读取器 |
该图着重显示了数据读取器如何存在于创建它的应用程序域中,以及对它的所有访问如何在客户端和服务器应用程序域之间产生往返行程。这意味着,只有当数据读取器在调用方所在的同一应用程序域中执行时,数据访问方法才应当返回这些数据读取器。在使用数据读取器时,还有其他两个需要考虑的问题。
首先,在从数据访问类中的方法返回数据读取器时,需要考虑与该数据读取器相关联的连接对象的生存期。默认情况下,在调用方循环访问数据读取器时,连接会保持繁忙。遗憾的是,当调用方完成工作时,连接将保持开启状态,因此不会返回到连接池(如果启用连接池的话)。但是,您可以通过将CommandBehavior.CloseConnection枚举值传递给命令对象的ExecuteReader方法,来指示数据读取器在它的Close方法被调用时关闭连接。
其次,为了将表示层从特定的Framework data provider(例如SqlClient或OleDb)分离,调用代码应当使用IDataReader接口而不是具体类型(例如SqlDataReader)来引用返回值。这样,如果应用程序从Oracle移植到SQL Server后端,并且数据访问类中的方法的返回类型进行了更改,则表示层无须更改。如果您希望数据访问类返回XML,则可以从System.Xml命名空间中的XmlDocument和XmlReader类(它们类似于DataSet和IDataReader)中进行选择。换句话说,当数据要从它的源断开连接时,您的方法应当返回XmlDocument(或XmlDataDocument),而XmlReader可以用来对XML数据进行流式访问。
最后,您还可以决定用公共属性返回自定义类。这些类可以用Serialization属性标记,以便能够跨应用程序域进行复制。另外,如果您要从方法中返回多个对象,则可能需要强类型集合类。
Imports System.Xml.Serialization
<Serializable()> _ Public Class Book : Implements IComparable
<XmlAttributeAttribute()> Public ProductID As Integer Public ISBN As String Public Title As String Public Author As String Public UnitCost As Decimal Public Description As String Public PubDate As Date
Public Function CompareTo(ByVal o As Object) As Integer _ Implements IComparable.CompareTo
Dim b As Book = CType(o, Book) Return Me.Title.CompareTo(b.Title) End Function
End Class
Public NotInheritable Class BookCollection : Inherits ArrayList
Default Public Shadows Property Item(ByVal productId As Integer) As Book
Get Return Me(IndexOf(productId)) End Get Set(ByVal Value As Book) Me(IndexOf(productId)) = Value End Set End Property
Public Overloads Function Contains(ByVal productId As Integer) As Boolean Return (-1 <> IndexOf(productId)) End Function
Public Overloads Function IndexOf(ByVal productId As Integer) As Integer Dim index As Integer = 0 Dim item As Book
For Each item In Me If item.ProductID = productId Then Return index End If index = index + 1 Next Return -1 End Function
Public Overloads Sub RemoveAt(ByVal productId As Integer) RemoveAt(IndexOf(productId)) End Sub
Public Shadows Function Add(ByVal value As Book) As Integer Return MyBase.Add(value) End Function
End Class |
图6 使用一个自定义类
图6包含了一个简单的Book类及其关联的集合类的示例。您会发现,Book类用Serializable进行了标记,以便跨应用程序域启用“按值(by value)”语义。该类实现了IComparable接口,以便当它包含在集合类中的时候,它将在默认情况下按Title排序。BookCollection类派生自System.Collections命名空间中的ArrayList,并且遮蔽了Item属性和Add方法,以便将集合限制为仅包含Book对象。通过使用自定义类,您可以获得对数据表示方法的完全控制,通过强类型化和IntelliSense提高开发人员的工作效率,并且消除对ADO.NET的调用方依赖性。但是,由于.NET Framework不包含任何与对象相关的映射技术(除了本质上是派生的DataSet类的类型化DataSet对象以外),因此该方法需要更多的代码。在这些情况下,您通常要在数据访问类中创建一个数据读取器,并且使用它来填充自定义类。