對“三層結構”的深入理解——怎樣纔算是一個符合“三層結構”的Web應用程序?
ASP.NET Web應用程序解決方案中,並不是說有aspx文件、有dll文件、還有數據庫,就是“三層結構”的Web應用程序,這樣的說法是不對的。也並不是說沒有對數據庫進行操作,就不是“三層結構”的。其實“三層結構”是功能實現上的三層。例如,在微軟的ASP.NET示範實例“Duwamish7”中,“表現層”被放置在“Web”項目中,“中間業務層”是放置在“BusinessFacade”項目中,“數據訪問層”則是放置在“DataAccess”項目中……而在微軟的另一個ASP.NET示範實例“PetShop3.0”中,“表現層”被放置在“Web”項目中,“中間業務層”是放置在“BLL”項目中,而“數據訪問層”則是放置在“SQLServerDAL”和“OracleDAL”兩個項目中。在Bincess.CN彬月論壇中,“表現層”是被放置在“WebForum”項目中,“中間業務(服務)層”是被放置在“InterService”項目中,而“數據訪問層”是被放置在“SqlServerTask”項目中。
“三層結構”是什麼?
“三層結構”一詞中的“三層”是指:“表現層”、“中間業務層”、“數據訪問層”。其中:
n 表 現 層:位於最外層(最上層),離用戶最近。用於顯示數據和接收用戶輸入的數據,爲用戶提供一種交互式操作的界面。
n 中間業務層:負責處理用戶輸入的信息,或者是將這些信息發送給數據訪問層進行保存,或者是調用數據訪問層中的函數再次讀出這些數據。中間業務層也可以包括一些對“商業邏輯”描述代碼在裏面。
n 數據訪問層:僅實現對數據的保存和讀取操作。數據訪問,可以訪問數據庫系統、二進制文件、文本文檔或是XML文檔。
對依賴方向的研究將是本文的重點,數值返回方向基本上是沒有變化的。
在一個
如果只以分層的設計角度看,Duwamish7要比PetShop3.0複雜一些!而如果較爲全面的比較二者,PetShop3.0則顯得比較複雜。但我們先不討論這些,對PetShop3.0和Duwamish7的研究,並不是本文的重點。現在的問題就是:既然“三層結構”已經被分派到各自的項目中,那麼剩下來的項目是做什麼的呢?例如PetShop3.0中的“Model”、“IDAL”、“DALFactory”這三個項目,再例如Duwamish7中的“Common”項目,還有就是在Bincess.CN彬月論壇中的“Classes”、“DbTask”、這兩個項目。它們究竟是做什麼用的呢?
對“三層結構”的深入理解——從一家小餐館說起
一個“三層結構”的Web應用程序,就好象是一家小餐館。
n 表 現 層,所有的.aspx頁面就好像是這家餐館的菜譜。
n 中間業務層,就像是餐館的服務生。
n 數據訪問層,就像是餐館的大廚師傅。
n 而我們這些網站瀏覽者,就是去餐館吃飯的吃客了……
我們去一家餐館吃飯,首先得看他們的菜譜,然後喚來服務生,告訴他我們想要吃的菜餚。服務生記下來以後,便會馬上去通知大廚師傅要烹製這些菜。大廚師傅收到通知後,馬上起火燒菜。過了不久,服務生便把一道一道香噴噴的、熱氣騰騰的美味端到我們的桌位上——
而我們訪問一個基於asp.net技術的網站的時候,首先打開的是一個aspx頁面。這個aspx頁面的後臺程序會去調用中間業務層的相應函數來獲取結果。中間業務層又會去調用數據訪問層的相應函數來獲取結果。
爲什麼需要“三層結構”?——初探,就從數據庫的升遷開始
一個站點中,訪問數據庫的程序代碼散落在各個頁面中,就像夜空中的星星一樣繁多。這樣一動百動的維護,難度可想而知。最難以忍受的是,對這種維護工作的投入,是沒有任何價值的……
有一個比較好的解決辦法,那就是將訪問數據庫的代碼全部都放在一個程序文件裏。這樣,數據庫平臺一旦發生變化,那麼只需要集中修改這一個文件就可以了。我想有點開發經驗的人,都會想到這一步的。這種“以不變應萬變”的做法其實是簡單的“門面模式”的應用。如果把一個網站比喻成一家大飯店,那麼“門面模式”中的“門面”,就像是飯店的服務生,而一個網站的瀏覽者,就像是一個來賓。來賓只需要發送命令給服務生,然後服務生就會按照命令辦事。至於服務生經歷了多少辛苦才把事情辦成?那個並不是來賓感興趣的事情,來賓們只要求服務生儘快把自己交待事情辦完。我們就把ListLWord.aspx.cs程序就看成是一個來賓發出的命令,而把新加入的LWordTask.cs程序看成是一個飯店服務生,那麼來賓發出的命令就是:
“給我讀出留言板數據庫中的數據,填充到DataSet數據集中並顯示出來!”
而服務生接到命令後,就會依照執行。而PostLWord.aspx.cs程序,讓服務生做的是:
“把我的留言內容寫入到數據庫中!”
而服務生接到命令後,就會依照執行。這就是TraceLWord2!可以在CodePackage/TraceLWord2目錄中找到——
把所有的有關數據訪問的代碼都放到LWordTask.cs文件裏,LWordTask.cs程序文件如下:
#001 using System;
#002 using System.Data;
#003 using System.Data.OleDb; // 需要操作 Access 數據庫
#004 using System.Web;
#005
#006 namespace TraceLWord2
#007 {
#008 /// <summary>
#009 /// LWordTask 數據庫任務類
#010 /// </summary>
#011 public class LWordTask
#012 {
#013 // 數據庫連接字符串
#014 private const string DB_CONN=@"PROVIDER=Microsoft.Jet.OLEDB.4.0;
DATA Source=C:/DbFs/TraceLWordDb.mdb";
#015
#016 /// <summary>
#017 /// 讀取數據庫表 LWord,並填充 DataSet 數據集
#018 /// </summary>
#019 /// <param name="ds">填充目標數據集</param>
#020 /// <param name="tableName">表名稱</param>
#021 /// <returns>記錄行數</returns>
#022 public int ListLWord(DataSet ds, string tableName)
#023 {
#024 string cmdText="SELECT * FROM [LWord] ORDER BY [LWordID] DESC";
#025
#026 OleDbConnection dbConn=new OleDbConnection(DB_CONN);
#027 OleDbDataAdapter dbAdp=new OleDbDataAdapter(cmdText, dbConn);
#028
#029 int count=dbAdp.Fill(ds, tableName);
#030
#031 return count;
#032 }
#033
#034 /// <summary>
#035 /// 發送留言信息到數據庫
#036 /// </summary>
#037 /// <param name="textContent">留言內容</param>
#038 public void PostLWord(string textContent)
#039 {
#040 // 留言內容不能爲空
#041 if(textContent==null || textContent=="")
#042 throw new Exception("留言內容爲空");
#043
#044 string cmdText="INSERT INTO [LWord]([TextContent]) VALUES(@TextContent)";
#045
#046 OleDbConnection dbConn=new OleDbConnection(DB_CONN);
#047 OleDbCommand dbCmd=new OleDbCommand(cmdText, dbConn);
#048
#049 // 設置留言內容
#050 dbCmd.Parameters.Add(new OleDbParameter("@TextContent", OleDbType.LongVarWChar));
#051 dbCmd.Parameters["@TextContent"].Value=textContent;
#052
#053 try
#054 {
#055 dbConn.Open();
#056 dbCmd.ExecuteNonQuery();
#057 }
#058 catch
#059 {
#060 throw;
#061 }
#062 finally
#063 {
#064 dbConn.Close();
#065 }
#066 }
#067 }
#068 }
如果將數據庫從Access 2000修改爲SQL Server 2000,那麼只需要修改LWordTask.cs這一個文件。如果LWordTask.cs文件太大,也可以把它切割成幾個文件或“類”。如果被切割成的“類”還是很多,也可以把這些訪問數據庫的類放到一個新建的“項目”裏。當然,原來的ListLWord.aspx.cs文件應該作以修改,LWord_DataBind函數被修改成:
...
#046 private void LWord_DataBind()
#047 {
#048 DataSet ds=new DataSet();
#049 (new LWordTask()).ListLWord(ds, @"LWordTable");
#050
#051 m_lwordListCtrl.DataSource=ds.Tables[@"LWordTable"].DefaultView;
#052 m_lwordListCtrl.DataBind();
#053 }
...
原來的PostLWord.aspx.cs文件也應作以修改,Post_ServerClick函數被修改成:
...
#048 private void Post_ServerClick(object sender, EventArgs e)
#049 {
#050 // 獲取留言內容
#051 string textContent=this.m_txtContent.Value;
#052
#053 (new LWordTask()).PostLWord(textContent);
#054
#055 // 跳轉到留言顯示頁面
#056 Response.Redirect("ListLWord.aspx", true);
#057 }
...
從前面的程序段中可以看出,ListLWord.aspx.cs和PostLWord.aspx.cs這兩個文件已經找不到和數據庫相關的代碼了。只看到一些和LWordTask類有關係的代碼,這就符合了“設計模式”中的一種重要原則:“迪米特法則”。“迪米特法則”主要是說:讓一個“類”與儘量少的其它的類發生關係。在TraceLWord1中,ListLWord.aspx.cs這個類和OleDbConnection及OleDbDataAdapter都發生了關係,所以它破壞了“迪米特法則”。利用一個“中間人”是“迪米特法則”解決問題的辦法,這也是“門面模式”必須遵循的原則。下面就引出這個LWordTask門面類的示意圖:
ListLWord.aspx.cs和PostLWord.aspx.cs兩個文件對數據庫的訪問,全部委託LWordTask類這個“中間人”來辦理。利用“門面模式”,將頁面類和數據庫類進行隔離。這樣就作到了頁面類不依賴於數據庫的效果。以一段比較簡單的代碼來描述這三個程序的關係:
public class ListLWord
{
private void LWord_DataBind()
{
(new LWordTask()).ListLWord( ... );
}
}
public class PostLWord
{
private void Post_ServerClick(object sender, EventArgs e)
{
(new LWordTask()).PostLWord( ... );
}
}
public class LWordTask
{
public DataSet ListLWord(DataSet ds)...
public void PostLWord(string textContent)...
}
應用中間業務層,實現“三層結構”
前面這種分離數據訪問代碼的形式,可以說是一種“三層結構”的簡化形式。因爲它沒有“中間業務層”也可以稱呼它爲“二層結構”。一個真正的“三層”程序,是要有“中間業務層”的,而它的作用是連接“外觀層”和“數據訪問層”。換句話說:“外觀層”的任務先委託給“中間業務層”來辦理,然後“中間業務層”再去委託“數據訪問層”來辦理……
那麼爲什麼要應用“中間業務層”呢?“中間業務層”的用途有很多,例如:驗證用戶輸入數據、緩存從數據庫中讀取的數據等等……但是,“中間業務層”的實際目的是將“數據訪問層”的最基礎的存儲邏輯組合起來,形成一種業務規則。例如:“在一個購物網站中有這樣的一個規則:在該網站第一次購物的用戶,系統爲其自動註冊”。這樣的業務邏輯放在中間層最合適:
在“數據訪問層”中,最好不要出現任何“業務邏輯”!也就是說,要保證“數據訪問層”的中的函數功能的原子性!即最小性和不可再分。“數據訪問層”只管負責存儲或讀取數據就可以了。
在新TraceLWord3中,應用了“企業級模板項目”。把原來的LWordTask.cs,並放置到一個單一的項目裏,項目名稱爲:AccessTask。解決方案中又新建了一個名稱爲:InterService的項目,該項目中包含一個LWordService.cs程序文件,它便是“中間業務層”程序。