摘要:大多數使用.NET框架組件工作的開發人員的一個核心工作是實現數據訪問功能,他們建立的數據訪問層(data aclearcase/" target="_blank" >ccess 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.SqlClient Namespace ACME.Data Protected Sub New(ByVal connect As String) Protected ReadOnly Property Connection() As SqlConnection Public Sub Dispose() Implements IDisposable.Dispose End Class |
在列表中可以看到,對DALBase類作了MustInherit標記(C#中的抽象),以確保它在繼承關系中使用。接著該類在公共構造函數中包括了一個實例化的私有SqlConnection對象,它接收連接字符串作為一個參數。當來自IDisposable接口的Dispose方法確保連接對象已經被配置了的時候,受保護的(protected)Connection屬性允許衍生類訪問該連接對象。
即使在下面簡化的例子中你也能開始看到抽象基類的用處:
Public Class WebData : Inherits DALBase Public Function GetOrders() As DataSet da.Fill(ds) |
在這種情況下,WebData類繼承自DALBase,結果就是不必擔心實例化SqlConnection對象,而是通過MyBase關鍵字(或者C#中的基關鍵字)簡單地把連接字符串傳遞給基類。WebData類的GetOrders方法能使用Me.Connection(在C#中是this.Connection)訪問受保護的屬性。雖然這個例子相對簡單,但是你將在規則2和3中看到基類也提供了其它的服務。
當數據訪問層必須在COM+環境中運行時抽象的基類很有用。在這種情況下,因為允許組件使用COM+的必要代碼復雜得多,所以更好的方式是建立一個如列表2所示的服務組件(serviced component)基類。
<ConstructionEnabled(True), _ Private _connection As SqlConnection Protected Overrides Sub Construct(ByVal s As String) Protected ReadOnly Property Connection() As SqlConnection |
在這段代碼中,DALServicedBase類包含的基本功能與列表1中的相同,但是加上了從System.EnterpriseServices名字空間的ServicedComponent的繼承,并且包括了一些屬性,指明組件支持對象構造、事務和靜態跟蹤。接著該基類仔細地捕捉組件服務管理器(Component Services Manager)中的構造字符串并且再次建立和暴露SqlConnection對象。我們要注意的是當一個類繼承自DALServicedBase時,它也繼承了屬性的設置。換句話說,一個衍生類的事務選項也設置為Supported。如果衍生類想重載這種行為,它能在類的層次重新定義該屬性。
此外,衍生類在適當情況下應該有利于自身重載和共享方法。使用重載的方法(一個方法有多個調用信號)在本質上有兩種情況。首先,它們在一個方法需要接受多種類型的參數時使用??蚣芙M件中的典型例子是System.Convert類的方法。例如ToString方法包含18個接受一個參數的重載方法,每個重載方法的類型不同。其次,重載的方法用于暴露參數數量不斷增長的信號,而不是不同類型的必要參數。在數據訪問層中這類重載變得效率很高,因為它能用于為數據檢索和修改暴露交替的信號。例如GetOrders方法可以重載,這樣一個信號不接受參數并返回所有訂單,但是附加的信號接受參數以表明調用程序希望檢索特定的顧客訂單,代碼如下:
Public Overloads Function GetOrders() As DataSet Public Overloads Function GetOrders(ByVal customerId As Integer) As DataSet |
這種情況下的一個好的實現技巧是抽象GetOrders方法的功能到一個能被每個重載信號調用的私有的或者受保護的方法中。
共享方法(C#中的靜態方法)也能用于暴露數據訪問類的所有實例能夠訪問的字段、屬性和方法。盡管共享成員不能與使用組件服務(Component Services)的類一起使用,但是對于在數據訪問類的共享構造函數中檢索并被所有實例讀取的只讀數據是有用的。使用共享成員讀/寫數據時要小心,因為為了訪問該共享數據,執行的多個線程可能會競爭。
規則2:堅持設計指導
隨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)
.NET框架組件包括一些類和構造,它們能輔助處理通常的與基礎結構相關的事務,例如裝置和異常處理。通過基類把這些概念與繼承組合起來將非常強大。例如,你能考慮一下System.Diagnostics名字空間中暴露的跟蹤功能。除了提供Trace和Debug類外,該名字空間還包括衍生自Switch和TraceListener的類。Switch類的BooleanSwitch和TraceSwitch能被配置用于打開和關閉應用程序和配置文件,在TraceSwitch中可以暴露多層次跟蹤。TraceListener類的TextWriterTraceListener和EventLogTraceListener分別將Trace和Debug方法的輸入定位到文本文件和事件日志。
這樣作的結果是給基類添加了跟蹤功能,使衍生類記錄消息日志更簡單。接著應用程序能使用配置文件控制是否允許跟蹤。你能包括一個BooleanSwitch類型的私有變量并在構造函數中實例化它來給列表1中的DALBase添加這個功能:
Public Sub New(ByVal connect As String) |
傳遞給BooleanSwitch的參數包括名字和描述。接著你能添加一個受保護的屬性打開和關閉開關,也能添加一個屬性使用Trace對象的WriteLineIf方法格式化并寫入跟蹤消息:
Protected Property TracingEnabled() As Boolean Protected Sub WriteTrace(ByVal message As String) |
通過這種途徑,衍生類自己并不知道開關(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(ByVal message As String) Public Sub New(ByVal message As String, ByVal innerException As |
你從中收益的第二個基礎結構是結構化異常處理(SEH)。在最基本的層次,數據訪問類能夠暴露它的衍生自System.ApplicationException 的Exception(異常)對象并能進一步暴露自定義成員。例如,列表4中顯示的DALException對象能用于包裝數據訪問類中的代碼產生的異常。接著基類能暴露一個受保護的方法包裝該異常,組裝自定義成員,并把它發回給調用程序,如下所示:
Protected Sub ThrowDALException(ByVal message As String, _ newMine.ConnectString = Me.Connection.ConnectionString |
使用這種方法,衍生類能簡單地調用受保護的方法,傳遞進去一個特定的數據異常(典型的有SqlException或者 OleDbException),該異常被截取并添加了從屬于特定數據域的消息?;愒贒ALException中包裝該異常并把它發回到調用程序。這就允許調用程序用一個Catch語句輕易地捕捉所有來自數據訪問類的異常。
作為選擇之一,你可以看一看MSDN上發布的"Exception Management Application Block Overview"。該框架組件通過一系列對象結合了異常和應用程序日志記錄。實際上,通過從.NET 框架組件提供的BaseApplicationException類衍生的自定義異常類能夠簡單地插入該框架組件。
規則4:仔細選擇外部界面
在你設計數據訪問類的方法時,需要考慮它們怎樣接受和返回數據。對大多數開發人員來說,主要有三個選擇:直接使用ADO.NET對象、使用XML、使用自定義類。
如果直接暴露ADO.NET對象,你能使用一到兩個編程模型。第一個包括數據集和數據表對象,它們對不連接數據訪問很有用。有很多關于數據集和與它關聯的數據表的文章,但是當你必須使用從下層數據存儲斷開的數據時它才最有用處。換句話說,數據集能在應用程序各層之間傳遞,即使那些層在物理上是分布式的,當業務和數據服務層放置在同一群服務器上并且與表現服務分開時也能使用。此外,數據集對象是通過基于XML的Web服務返回數據的理想方法,因為它們是可串行化的,因此能在SOAP回應消息中返回。
這與使用實現IDataReader接口的類(例如SqlDataReader 和OleDbDataReader)訪問數據不同。數據閱讀器(data reader)用只向前的,只讀的方式訪問數據。兩者之間最大的不同是數據集和數據表對象能在應用程序域之間傳遞,通過傳遞值(by value)實現,然而數據閱讀器能在各處傳遞,但是一般通過引用(by reference)實現。在列表5中,Read和GetValues在服務器過程中執行并且它們的返回值復制到客戶端。
圖1.遠程數據閱讀器
該圖顯示了數據閱讀器怎樣存活在應用程序域中,它在那兒它被建立,并且對它的所有訪問結果都在客戶端和服務器應用程序域之間的循環之中。這意味著當數據訪問方法在相同的應用程序域運行時,應該返回數據閱讀器作為調用者。
使用數據閱讀器時有兩個問題需要考慮。首先,當你從數據訪問類的一個方法返回數據閱讀器時,你必須考慮與數據閱讀器關聯的連接對象的生存期。默認情況是當調用程序通過數據閱讀器重復時連接仍然是忙的,不幸的是當調用程序結束后,連接仍然打開,因此它不返回到連接池(如果允許連接池)。但是,當通過傳遞CommandBehavior.CloseConnection 枚舉給command對象的ExecuteReader方法,連接的Close方法被調用時,你能命令數據閱讀器關閉它的連接。
其次,為了把表現層從特定的框架組件數據提供程序(例如SqlClient或者OleDb)中分離出來,調用代碼應該使用IDataReader接口(例如SqlDataReader)而不是具體類型來引用返回值。通過這種方法,如果應用程序后端從Oracle移植到 SQL Server,或者數據訪問類的一個方法的返回類型改變了,表現層也不需要更改。
如果你希望數據訪問類返回XML,你可以從System.Xml名字空間中的XmlDocument和XmlReader中選擇一個,它與數據集和IDataReader類似。換句話說,當數據從數據源斷開時你的方法應該返回一個XmlDocument(或者XmlDataDocument),然而XmlReader可用于訪問XML數據的流。
最后,你也能決定與公共屬性一起返回自定義類。這些類可以使用Serialization(串行化)屬性標記,這樣它們就能跨越應用程序域復制。另外,如果你從方法中返回多個對象,就需要強化類型(strongly typed)的集合類。
Imports System.Xml.Serialization <Serializable()> _ Public Function CompareTo(ByVal o As Object) As Integer _ Public NotInheritable Class BookCollection : Inherits ArrayList Public Overloads Function Contains(ByVal productId As Integer) As _ Public Overloads Function IndexOf(ByVal productId As Integer) As _ For Each item In Me Public Overloads Sub RemoveAt(ByVal productId As Integer) Public Shadows Function Add(ByVal value As Book) As Integer |
上列表(列表6)包含了一個簡單的Book類和與它關聯的集合類的例子。你能注意到Book類用Serializable做了標記,使它跨越應用程序域能使用"by value"語法。該類實現了IComparable接口,因此當它包含在一個集合類中的時候,默認情況下它將按Title排序。BookCollection類從System.Collections名字空間的ArrayList衍生,并且為了將該集合限制到Book對象而隱藏了Item屬性和ADD方法。
通過使用自定義類你完全地控制了數據的表現、開發人員的效率并且沒有依賴ADO.NET的調用。但是這種途徑需要更多的代碼,因為.NET框架組件沒有包含任何與對象相關的技術映射。在這種情況下,你應該在數據訪問類中建立一個數據讀取器并使用它來組合自定義類。
規則5:抽象.NET框架組件數據提供程序
最后一條規則說明了為什么和怎樣抽象數據訪問類內部使用的.NET框架組件數據提供程序(data provider)。先前我說過ADO.NET編程模型暴露了特定的.NET框架組件數據提供程序,包括SqlClient、OleDb和其它MSDN Online Web站點上可用的。但是這種設計的結果是提高性能,為數據提供程序暴露特定數據源功能的能力,它強迫你決定使用那種數據提供程序編碼。換句話說,開發人員典型地會選擇使用SqlClient或OleDb,接著在各自的名字空間直接對它們的類進行編程。
如果你想改變.NET框架組件數據提供程序,你必須重新編寫數據訪問方法。為了避免這種情況發生,你可以使用Abstract Factory設計模式。使用這種模式,你能建立一個簡單的類,它暴露方法來建立主要的.NET框架組件數據提供程序對象(command、connection、data adapter和parameter),而那些對象基于傳遞給構造函數的.NET框架組件數據提供程序的信息。列表7中的代碼就是這樣一個簡單的類。
public enum ProviderType :int {SqlClient = 0, OLEDB = 1} public class ProviderFactory { public ProviderFactory() {
// 為提供程序初始化類型 public ProviderType Provider { |
為了使用該類,數據訪問類的代碼必須對多個.NET框架組件數據提供程序實現的接口(包括IDbCommand、IDbConnection、IDataAdapter和IDataParameter)進行編程。例如,為了使用一個參數化存儲過程的返回值來填充數據集,必須在數據訪問類的某個方法中有下面的代碼:
Dim _pf As New ProviderFactory(ProviderType.SqlClient) Dim db As IDbDataAdapter = CType(da, IDbDataAdapter) Dim ds As New DataSet("Books") |
典型的情況是你在類的層次聲明ProviderFactory變量并在數據訪問類的構造函數中實例化它。另外,它的構造函數與從配置文件中讀取的提供程序一起組裝,而不應該是硬代碼。你可以想象,ProviderFactory是數據訪問類的一個重大的補充,并且能被包括進部件,分發給其它的開發人員。
結論
在Web服務時代將建立越來越多的應用程序操作來自獨立的應用程序層的數據。如果你遵循一些基本規則并形成習慣,編寫數據訪問代碼將更快、更容易,并且更能重新使用,把你的錯誤保存到服務器,允許你保持數據獨立。
原文轉自:http://www.anti-gravitydesign.com