摘要:大多数使用.NET框架组件工作的开发人员的一个核心工作是实现数据访问功能,他们建立的数据访问层(data access layer)是应用程序的精华部分。本文概述了使用Visual Studio .NET和.NET框架组件建立数据访问层需要考虑的五个想法。这些技巧包括通过使用基类(base class)利用面相对象技术和.NET框架组件基础结构,使类轻易继续,在决定显示方法和外部界面前仔细地检验需求。 假如你正在建立以数据为中心(data-centric)的.NET框架组件应用程序,你最终必须建立数据访问层。也许你知道在.NET框架组件中建立自己的代码有很多好处。因为它支持实现和接口(interface)继续,你的代码更轻易重复使用,非凡是被使用不同的框架组件兼容(Framework-compliant)语言的开发人员使用。本文我将概述为基于.NET框架组件的应用程序建立数据访问层的五条规则。
开始前,我必须提醒你建立的任何基于本文讨论的规则的数据访问层必须与传统Windows平台上开发人员喜欢的多层或者n层应用程序兼容。在这种结构中,表现层包含Web窗体、Windows窗体、调用与数据访问层的工作相应的事务层的xml服务代码。该层由多个数据访问类(data access classe)组成。换句话说,在事务处理协调不是必要的情况下,表现层将直接调用数据访问层。这种结构是传统的模型-视列表-控制程序(Model-View-Controller,MVC)模式的变体,在多种情况下被Visual Studio .NET和它暴露的控件采用。 规则1:使用面向对象特性
最基本的面向对象事务是建立一个使用实现继续的抽象类。这个基类可以包括你的所有数据访问类通过继续能够使用的服务。假如那些服务足够了,它们就能通过在整个组织的基类分布实现重复使用。例如最简单的情况是基类能够为衍生类处理连接的建立过程,如列表1所示。 Imports System.Data.SqlClientNamespace ACME.Data Public MustInherit Class DALBase : Implements IDisposable PRivate _connection As SqlConnectionProtected Sub New(ByVal connect As String) _connection = New SqlConnection(connect) End SubProtected ReadOnly Property Connection() As SqlConnection Get Return _connection End Get End PropertyPublic Sub Dispose() Implements IDisposable.Dispose _connection.Dispose() End SubEnd Class End Namespace 列表1.简单基类 在列表中可以看到,对DALBase类作了MustInherit标记(C#中的抽象),以确保它在继续关系中使用。接着该类在公共构造函数中包括了一个实例化的私有SqlConnection对象,它接收连接字符串作为一个参数。当来自IDisposable接口的Dispose方法确保连接对象已经被配置了的时候,受保护的(protected)Connection属性答应衍生类访问该连接对象。
即使在下面简化的例子中你也能开始看到抽象基类的用处: Public Class WebData : Inherits DALBase Public Sub New() MyBase.New(ConfigurationSettings.AppSettings("ConnectString")) End SubPublic Function GetOrders() As DataSet Dim da As New SqlDataAdapter("usp_GetOrders", Me.Connection) da.SelectCommand.CommandType = CommandType.StoredProcedure Dim ds As New DataSet()da.Fill(ds) Return ds End Function End Class 在这种情况下,WebData类继续自DALBase,结果就是不必担心实例化SqlConnection对象,而是通过MyBase要害字(或者C#中的基要害字)简单地把连接字符串传递给基类。WebData类的GetOrders方法能使用Me.Connection(在C#中是this.Connection)访问受保护的属性。虽然这个例子相对简单,但是你将在规则2和3中看到基类也提供了其它的服务。
当数据访问层必须在COM+环境中运行时抽象的基类很有用。在这种情况下,因为答应组件使用COM+的必要代码复杂得多,所以更好的方式是建立一个如列表2所示的服务组件(serviced component)基类。 <ConstrUCtionEnabled(True), _ Transaction(TransactionOption.Supported), _ EventTrackingEnabled(True)> _ Public MustInherit Class DALServicedBase : Inherits ServicedComponentPrivate _connection As SqlConnectionProtected Overrides Sub Construct(ByVal s As String)
_connection = New SqlConnection(s) End SubProtected ReadOnly Property Connection() As SqlConnection Get Return _connection End Get End Property End Class 列表2.服务组件基类 在这段代码中,DALServicedBase类包含的基本功能与列表1中的相同,但是加上了从System.EnterpriseServices名字空间的ServicedComponent的继续,并且包括了一些属性,指明组件支持对象构造、事务和静态跟踪。接着该基类仔细地捕捉组件服务治理器(Component Services Manager)中的构造字符串并且再次建立和暴露SqlConnection对象。我们要注重的是当一个类继续自DALServicedBase时,它也继续了属性的设置。换句话说,一个衍生类的事务选项也设置为Supported。假如衍生类想重载这种行为,它能在类的层次重新定义该属性。
此外,衍生类在适当情况下应该有利于自身重载和共享方法。使用重载的方法(一个方法有多个调用信号)在本质上有两种情况。首先,它们在一个方法需要接受多种类型的参数时使用。框架组件中的典型例子是System.Convert类的方法。例如ToString方法包含18个接受一个参数的重载方法,每个重载方法的类型不同。其次,重载的方法用于暴露参数数量不断增长的信号,而不是不同类型的必要参数。在数据访问层中这类重载变得效率很高,因为它能用于为数据检索和修改暴露交替的信号。例如GetOrders方法可以重载,这样一个信号不接受参数并返回所有订单,但是附加的信号接受参数以表明调用程序希望检索特定的顾客订单,代码如下: Public Overloads Function GetOrders() As DataSet Public Overloads Function GetOrders(ByVal customerId As Integer) As DataSet 这种情况下的一个好的实现技巧是抽象GetOrders方法的功能到一个能被每个重载信号调用的私有的或者受保护的方法中。
随Visual Studio .NET一起发布的在线文档中有一个叫"类库开发人员的设计指导(Design Guidelines for Class Library Developers)"的主题,它覆盖了类、属性和方法的名字转换,是重载的成员、构造函数和事件的补充模式。 你必须遵循名字转换的主要原因之一是.NET框架组件提供的跨语言(cross-language)继续。假如你在Visual Basic .NET中建立一个数据访问层基类,你想确保使用.NET框架组件兼容的其它语言的开发人员能继续它并轻易理解它怎样工作。通过坚持我概述的指导方针,你的名字转换和构造就不会是语言特定的(language specific)。例如,你可能注重到在本文例子的代码中第一个词小写,并加上intercaps是用于方法的参数的,每个词大写是用于方法的,基类使用Base标志来标识它是一个抽象类。
可以推测.NET框架组件设计指导都是普通设计模式,像Gang of Four (Addison-Wesley, 1995)写的Design Patterns记载的一样。例如.NET框架组件使用了Observer模式的一个变体,叫做Event模式,在类中暴露事件时你必须遵循它。 规则3:利用基础结构(Infrastructure)
这样作的结果是给基类添加了跟踪功能,使衍生类记录消息日志更简单。接着应用程序能使用配置文件控制是否答应跟踪。你能包括一个BooleanSwitch类型的私有变量并在构造函数中实例化它来给列表1中的DALBase添加这个功能: Public Sub New(ByVal connect As String) _connection = New SqlConnection(connect) _dalSwitch = New BooleanSwitch("DAL", "Data Access Code") End Sub 传递给BooleanSwitch的参数包括名字和描述。接着你能添加一个受保护的属性打开和关闭开关,也能添加一个属性使用Trace对象的WriteLineIf方法格式化并写入跟踪消息:
Protected Property TracingEnabled() As Boolean Get Return _dalSwitch.Enabled End Get Set(ByVal Value As Boolean) _dalSwitch.Enabled = Value End Set End PropertyProtected Sub WriteTrace(ByVal message As String) Trace.WriteLineIf(Me.TracingEnabled, Now & ": " & message) End Sub 通过这种途径,衍生类自己并不知道开关(switch)和监听(listener)类,当数据访问类产生一个有意义的信号时能够简单地调用WriteTrace方法。 <?xml version="1.0" encoding="utf-8" ?> <configuration> <system.diagnostics> <switches> <add name="DAL" value="1" /> </switches> <trace autoflush="true" indentsize="4"> <listeners> <add name="myListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="DALLog.txt" /> </listeners> </trace> </system.diagnostics> </configuration> 列表3.跟踪的配置文件 为了建立一个监听器并打开它,需要使用应用程序配置文件。列表3显示了一个简单的配置文件,它能够打开刚才显示的数据访问类开关,并通过myListener调用TextWriterTraceListener把输出定位到文件DALLog.txt中。当然,你能通过从TraceListener类衍生程序化地建立监听器并把该监听器直接包含在数据访问类中。 Public Class DALException : Inherits applicationException Public Sub New() MyBase.New() End SubPublic Sub New(ByVal message As String) MyBase.New(message) End SubPublic Sub New(ByVal message As String, ByVal innerException As Exception) MyBase.New(message, innerException) End Sub '在这儿添加自定义成员 Public ConnectString As String End Class 列表4.自定义异常类 你从中收益的第二个基础结构是结构化异常处理(SEH)。在最基本的层次,数据访问类能够暴露它的衍生自System.ApplicationException 的Exception(异常)对象并能进一步暴露自定义成员。例如,列表4中显示的DALException对象能用于包装数据访问类中的代码产生的异常。接着基类能暴露一个受保护的方法包装该异常,组装自定义成员,并把它发回给调用程序,如下所示: Protected Sub ThrowDALException(ByVal message As String, _ ByVal innerException As Exception) Dim newMine As New DALException(message, innerException)newMine.ConnectString = Me.Connection.ConnectionString Me.WriteTrace(message & "{" & innerException.Message & "}") Throw newMine End Sub 使用这种方法,衍生类能简单地调用受保护的方法,传递进去一个特定的数据异常(典型的有SqlException或者 OleDbException),该异常被截取并添加了从属于特定数据域的消息。基类在DALException中包装该异常并把它发回到调用程序。这就答应调用程序用一个Catch语句轻易地捕捉所有来自数据访问类的异常。
最后,你也能决定与公共属性一起返回自定义类。这些类可以使用Serialization(串行化)属性标记,这样它们就能跨越应用程序域复制。另外,假如你从方法中返回多个对象,就需要强化类型(strongly typed)的集合类。 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 DatePublic 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 ClassPublic 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 PropertyPublic Overloads Function Contains(ByVal productId As Integer) As _ Boolean Return (-1 <> IndexOf(productId)) End FunctionPublic Overloads Function IndexOf(ByVal productId As Integer) As _ Integer Dim index As Integer = 0 Dim item As BookFor Each item In Me If item.ProductID = productId Then Return index End If index = index + 1 Next Return -1 End FunctionPublic Overloads Sub RemoveAt(ByVal productId As Integer) RemoveAt(IndexOf(productId)) End SubPublic 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衍生,并且为了将该集合限制到Book对象而隐藏了Item属性和ADD方法。
public IDbConnection CreateConnection(string connect) { IDbConnection c; c = (IDbConnection)Activator.CreateInstance(_conType[(int)_pType], false); c.ConnectionString = connect; return c; }
public IDbCommand CreateCommand(string cmdText, IDbConnection connection) { IDbCommand c; c = (IDbCommand)Activator.CreateInstance(_comType[(int)_pType], false); c.CommandText = cmdText; c.Connection = connection; return c; } } 列表7. ProviderFactory 为了使用该类,数据访问类的代码必须对多个.NET框架组件数据提供程序实现的接口(包括IDbCommand、IDbConnection、IDataAdapter和IDataParameter)进行编程。例如,为了使用一个参数化存储过程的返回值来填充数据集,必须在数据访问类的某个方法中有下面的代码: Dim _pf As New ProviderFactory(ProviderType.SqlClient) Dim cn As IDbConnection = _pf.CreateConnection(_connect) Dim da As IDataAdapter = _pf.CreateDataAdapter("usp_GetBook", cn)Dim db As IDbDataAdapter = CType(da, IDbDataAdapter) db.SelectCommand.CommandType = CommandType.StoredProcedure db.SelectCommand.Parameters.Add(_pf.CreateParameter("@productId",DbType.Int32, id))Dim ds As New DataSet("Books") da.Fill(ds) 典型的情况是你在类的层次声明ProviderFactory变量并在数据访问类的构造函数中实例化它。另外,它的构造函数与从配置文件中读取的提供程序一起组装,而不应该是硬代码。你可以想象,ProviderFactory是数据访问类的一个重大的补充,并且能被包括进部件,分发给其它的开发人员。 结论