摘要:大多數(shù)使用.NET框架組件工作的開發(fā)人員的一個(gè)核心工作是實(shí)現(xiàn)數(shù)據(jù)訪問功能,他們建立的數(shù)據(jù)訪問層(data access layer)是應(yīng)用程序的精華部分。本文概述了使用Visual Studio .NET和.NET框架組件建立數(shù)據(jù)訪問層需要考慮的五個(gè)想法。這些技巧包括通過使用基類(base class)利用面相對象技術(shù)和.NET框架組件基礎(chǔ)結(jié)構(gòu),使類輕易繼續(xù),在決定顯示方法和外部界面前仔細(xì)地檢驗(yàn)需求。 假如你正在建立以數(shù)據(jù)為中心(data-centric)的.NET框架組件應(yīng)用程序,你最終必須建立數(shù)據(jù)訪問層。也許你知道在.NET框架組件中建立自己的代碼有很多好處。因?yàn)樗С謱?shí)現(xiàn)和接口(interface)繼續(xù),你的代碼更輕易重復(fù)使用,非凡是被使用不同的框架組件兼容(Framework-compliant)語言的開發(fā)人員使用。本文我將概述為基于.NET框架組件的應(yīng)用程序建立數(shù)據(jù)訪問層的五條規(guī)則。
開始前,我必須提醒你建立的任何基于本文討論的規(guī)則的數(shù)據(jù)訪問層必須與傳統(tǒng)Windows平臺(tái)上開發(fā)人員喜歡的多層或者n層應(yīng)用程序兼容。在這種結(jié)構(gòu)中,表現(xiàn)層包含Web窗體、Windows窗體、調(diào)用與數(shù)據(jù)訪問層的工作相應(yīng)的事務(wù)層的xml服務(wù)代碼。該層由多個(gè)數(shù)據(jù)訪問類(data access classe)組成。換句話說,在事務(wù)處理協(xié)調(diào)不是必要的情況下,表現(xiàn)層將直接調(diào)用數(shù)據(jù)訪問層。這種結(jié)構(gòu)是傳統(tǒng)的模型-視列表-控制程序(Model-View-Controller,MVC)模式的變體,在多種情況下被Visual Studio .NET和它暴露的控件采用。 規(guī)則1:使用面向?qū)ο筇匦?BR> 最基本的面向?qū)ο笫聞?wù)是建立一個(gè)使用實(shí)現(xiàn)繼續(xù)的抽象類。這個(gè)基類可以包括你的所有數(shù)據(jù)訪問類通過繼續(xù)能夠使用的服務(wù)。假如那些服務(wù)足夠了,它們就能通過在整個(gè)組織的基類分布實(shí)現(xiàn)重復(fù)使用。例如最簡單的情況是基類能夠?yàn)檠苌愄幚磉B接的建立過程,如列表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標(biāo)記(C#中的抽象),以確保它在繼續(xù)關(guān)系中使用。接著該類在公共構(gòu)造函數(shù)中包括了一個(gè)實(shí)例化的私有SqlConnection對象,它接收連接字符串作為一個(gè)參數(shù)。當(dāng)來自IDisposable接口的Dispose方法確保連接對象已經(jīng)被配置了的時(shí)候,受保護(hù)的(protected)Connection屬性答應(yīng)衍生類訪問該連接對象。
即使在下面簡化的例子中你也能開始看到抽象基類的用處: 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類繼續(xù)自DALBase,結(jié)果就是不必?fù)?dān)心實(shí)例化SqlConnection對象,而是通過MyBase要害字(或者C#中的基要害字)簡單地把連接字符串傳遞給基類。WebData類的GetOrders方法能使用Me.Connection(在C#中是this.Connection)訪問受保護(hù)的屬性。雖然這個(gè)例子相對簡單,但是你將在規(guī)則2和3中看到基類也提供了其它的服務(wù)。
當(dāng)數(shù)據(jù)訪問層必須在COM+環(huán)境中運(yùn)行時(shí)抽象的基類很有用。在這種情況下,因?yàn)榇饝?yīng)組件使用COM+的必要代碼復(fù)雜得多,所以更好的方式是建立一個(gè)如列表2所示的服務(wù)組件(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.服務(wù)組件基類 在這段代碼中,DALServicedBase類包含的基本功能與列表1中的相同,但是加上了從System.EnterpriseServices名字空間的ServicedComponent的繼續(xù),并且包括了一些屬性,指明組件支持對象構(gòu)造、事務(wù)和靜態(tài)跟蹤。接著該基類仔細(xì)地捕捉組件服務(wù)治理器(Component Services Manager)中的構(gòu)造字符串并且再次建立和暴露SqlConnection對象。我們要注重的是當(dāng)一個(gè)類繼續(xù)自DALServicedBase時(shí),它也繼續(xù)了屬性的設(shè)置。換句話說,一個(gè)衍生類的事務(wù)選項(xiàng)也設(shè)置為Supported。假如衍生類想重載這種行為,它能在類的層次重新定義該屬性。
此外,衍生類在適當(dāng)情況下應(yīng)該有利于自身重載和共享方法。使用重載的方法(一個(gè)方法有多個(gè)調(diào)用信號(hào))在本質(zhì)上有兩種情況。首先,它們在一個(gè)方法需要接受多種類型的參數(shù)時(shí)使用。框架組件中的典型例子是System.Convert類的方法。例如ToString方法包含18個(gè)接受一個(gè)參數(shù)的重載方法,每個(gè)重載方法的類型不同。其次,重載的方法用于暴露參數(shù)數(shù)量不斷增長的信號(hào),而不是不同類型的必要參數(shù)。在數(shù)據(jù)訪問層中這類重載變得效率很高,因?yàn)樗苡糜跒閿?shù)據(jù)檢索和修改暴露交替的信號(hào)。例如GetOrders方法可以重載,這樣一個(gè)信號(hào)不接受參數(shù)并返回所有訂單,但是附加的信號(hào)接受參數(shù)以表明調(diào)用程序希望檢索特定的顧客訂單,代碼如下: Public Overloads Function GetOrders() As DataSet Public Overloads Function GetOrders(ByVal customerId As Integer) As DataSet 這種情況下的一個(gè)好的實(shí)現(xiàn)技巧是抽象GetOrders方法的功能到一個(gè)能被每個(gè)重載信號(hào)調(diào)用的私有的或者受保護(hù)的方法中。
隨Visual Studio .NET一起發(fā)布的在線文檔中有一個(gè)叫"類庫開發(fā)人員的設(shè)計(jì)指導(dǎo)(Design Guidelines for Class Library Developers)"的主題,它覆蓋了類、屬性和方法的名字轉(zhuǎn)換,是重載的成員、構(gòu)造函數(shù)和事件的補(bǔ)充模式。 你必須遵循名字轉(zhuǎn)換的主要原因之一是.NET框架組件提供的跨語言(cross-language)繼續(xù)。假如你在Visual Basic .NET中建立一個(gè)數(shù)據(jù)訪問層基類,你想確保使用.NET框架組件兼容的其它語言的開發(fā)人員能繼續(xù)它并輕易理解它怎樣工作。通過堅(jiān)持我概述的指導(dǎo)方針,你的名字轉(zhuǎn)換和構(gòu)造就不會(huì)是語言特定的(language specific)。例如,你可能注重到在本文例子的代碼中第一個(gè)詞小寫,并加上intercaps是用于方法的參數(shù)的,每個(gè)詞大寫是用于方法的,基類使用Base標(biāo)志來標(biāo)識(shí)它是一個(gè)抽象類。
可以推測.NET框架組件設(shè)計(jì)指導(dǎo)都是普通設(shè)計(jì)模式,像Gang of Four (Addison-Wesley, 1995)寫的Design Patterns記載的一樣。例如.NET框架組件使用了Observer模式的一個(gè)變體,叫做Event模式,在類中暴露事件時(shí)你必須遵循它。 規(guī)則3:利用基礎(chǔ)結(jié)構(gòu)(Infrastructure)
這樣作的結(jié)果是給基類添加了跟蹤功能,使衍生類記錄消息日志更簡單。接著應(yīng)用程序能使用配置文件控制是否答應(yīng)跟蹤。你能包括一個(gè)BooleanSwitch類型的私有變量并在構(gòu)造函數(shù)中實(shí)例化它來給列表1中的DALBase添加這個(gè)功能: Public Sub New(ByVal connect As String) _connection = New SqlConnection(connect) _dalSwitch = New BooleanSwitch("DAL", "Data Access Code") End Sub 傳遞給BooleanSwitch的參數(shù)包括名字和描述。接著你能添加一個(gè)受保護(hù)的屬性打開和關(guān)閉開關(guān),也能添加一個(gè)屬性使用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 通過這種途徑,衍生類自己并不知道開關(guān)(switch)和監(jiān)聽(listener)類,當(dāng)數(shù)據(jù)訪問類產(chǎn)生一個(gè)有意義的信號(hào)時(shí)能夠簡單地調(diào)用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.跟蹤的配置文件 為了建立一個(gè)監(jiān)聽器并打開它,需要使用應(yīng)用程序配置文件。列表3顯示了一個(gè)簡單的配置文件,它能夠打開剛才顯示的數(shù)據(jù)訪問類開關(guān),并通過myListener調(diào)用TextWriterTraceListener把輸出定位到文件DALLog.txt中。當(dāng)然,你能通過從TraceListener類衍生程序化地建立監(jiān)聽器并把該監(jiān)聽器直接包含在數(shù)據(jù)訪問類中。 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.自定義異常類 你從中收益的第二個(gè)基礎(chǔ)結(jié)構(gòu)是結(jié)構(gòu)化異常處理(SEH)。在最基本的層次,數(shù)據(jù)訪問類能夠暴露它的衍生自System.ApplicationException 的Exception(異常)對象并能進(jìn)一步暴露自定義成員。例如,列表4中顯示的DALException對象能用于包裝數(shù)據(jù)訪問類中的代碼產(chǎn)生的異常。接著基類能暴露一個(gè)受保護(hù)的方法包裝該異常,組裝自定義成員,并把它發(fā)回給調(diào)用程序,如下所示: 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 使用這種方法,衍生類能簡單地調(diào)用受保護(hù)的方法,傳遞進(jìn)去一個(gè)特定的數(shù)據(jù)異常(典型的有SqlException或者 OleDbException),該異常被截取并添加了從屬于特定數(shù)據(jù)域的消息。基類在DALException中包裝該異常并把它發(fā)回到調(diào)用程序。這就答應(yīng)調(diào)用程序用一個(gè)Catch語句輕易地捕捉所有來自數(shù)據(jù)訪問類的異常。
最后,你也能決定與公共屬性一起返回自定義類。這些類可以使用Serialization(串行化)屬性標(biāo)記,這樣它們就能跨越應(yīng)用程序域復(fù)制。另外,假如你從方法中返回多個(gè)對象,就需要強(qiáng)化類型(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)包含了一個(gè)簡單的Book類和與它關(guān)聯(lián)的集合類的例子。你能注重到Book類用Serializable做了標(biāo)記,使它跨越應(yīng)用程序域能使用"by value"語法。該類實(shí)現(xiàn)了IComparable接口,因此當(dāng)它包含在一個(gè)集合類中的時(shí)候,默認(rèn)情況下它將按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 為了使用該類,數(shù)據(jù)訪問類的代碼必須對多個(gè).NET框架組件數(shù)據(jù)提供程序?qū)崿F(xiàn)的接口(包括IDbCommand、IDbConnection、IDataAdapter和IDataParameter)進(jìn)行編程。例如,為了使用一個(gè)參數(shù)化存儲(chǔ)過程的返回值來填充數(shù)據(jù)集,必須在數(shù)據(jù)訪問類的某個(gè)方法中有下面的代碼: 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變量并在數(shù)據(jù)訪問類的構(gòu)造函數(shù)中實(shí)例化它。另外,它的構(gòu)造函數(shù)與從配置文件中讀取的提供程序一起組裝,而不應(yīng)該是硬代碼。你可以想象,ProviderFactory是數(shù)據(jù)訪問類的一個(gè)重大的補(bǔ)充,并且能被包括進(jìn)部件,分發(fā)給其它的開發(fā)人員。 結(jié)論