淺談“三層結構”原理與用意
2005年02月28日,AfritXia撰寫
2006年12月28日,AfritXia第一次修改
序
在剛剛步入“多層結構”Web應用程序開發的時候,我閱讀過幾篇關於“asp.net三層結構開發”的文章。但其多半都是對PetShop3.0和Duwamish7的局部剖析或者是學習筆記。對“三層結構”通體分析的學術文章幾乎沒有。
2005年2月11日,Bincess BBS彬月論壇開始試運行。不久之後,我寫了一篇題目爲《淺談“三層結構”原理與用意》的文章。舊版文章以彬月論壇程序中的部分代碼舉例,通過全局視角闡述了什麼是“三層結構”的開發模式?爲什麼要這樣做?怎樣做?……而在這篇文章的新作中,配合這篇文章我寫了7個程序實例(TraceLWord1~TraceLWord7留言板)以幫助讀者理解“三層結構”應用程序。這些程序示例可以在隨帶的CodePackage目錄中找到——
對於那些有豐富經驗的Web應用程序開發人員,他們認爲文章寫的通俗易懂,很值得一讀。可是對於asp.net初學者,特別是沒有任何開發經驗的人,文章閱讀起來就感到非常困難,不知文章所云。甚至有些讀者對“三層結構”的認識更模糊了……
關於“多層結構”開發模式,存在這樣一種爭議:一部分學者認爲“多層結構”與“面向對象的程序設計思想”有着非常緊密的聯繫。而另外一部分學者卻認爲二者之間並無直接聯繫。寫作這篇文章並不是要終結這種爭議,其行文目的是希望讀者能夠明白:在使用asp.net進行Web應用程序開發時,實現“多層結構”開發模式的方法、原理及用意。要順利的閱讀這篇文章,希望讀者能對“面向對象的程序設計思想”有一定深度的認識,最好能懂一些“設計模式”的知識。如果你並不瞭解前面這些,那麼這篇文章可能並不適合你現在閱讀。不過,無論這篇文章面對的讀者是誰,我都會盡量將文章寫好。我希望這篇文章能成爲學習“三層結構”設計思想的經典文章!
“三層結構”是什麼?
“三層結構”一詞中的“三層”是指:“表現層”、“中間業務層”、“數據訪問層”。其中:
n 表 現 層:位於最外層(最上層),離用戶最近。用於顯示數據和接收用戶輸入的數據,爲用戶提供一種交互式操作的界面。
n 中間業務層:負責處理用戶輸入的信息,或者是將這些信息發送給數據訪問層進行保存,或者是調用數據訪問層中的函數再次讀出這些數據。中間業務層也可以包括一些對“商業邏輯”描述代碼在裏面。
n 數據訪問層:僅實現對數據的保存和讀取操作。數據訪問,可以訪問數據庫系統、二進制文件、文本文檔或是XML文檔。
對依賴方向的研究將是本文的重點,數值返回方向基本上是沒有變化的。
爲什麼需要“三層結構”?——通常的設計方式
在一個大型的Web應用程序中,如果不分以層次,那麼在將來的升級維護中會遇到很大的麻煩。但在這篇文章裏我只想以一個簡單的留言板程序爲示例,說明通常設計方式的不足——
功能說明:
ListLWord.aspx(後臺程序文件 ListLWord.aspx.cs)列表顯示數據庫中的每條留言。
PostLWord.aspx(後臺程序文件 PostLWord.aspx.cs)發送留言到數據庫。
更完整的示例代碼,可以到CodePackage/TraceLWord1目錄中找到。數據庫中,僅含有一張數據表,其結構如下:
字段名稱
|
數據類型
|
默認值
|
備註說明
|
[LWordID]
|
INT
|
NOT NULL IDENTITY(1, 1)
|
留言記錄編號
|
[TextContent]
|
NText
|
N’’
|
留言內容
|
[PostTime]
|
DateTime
|
GetDate()
|
留言發送時間,默認值爲當前時間
|
ListLWord.aspx 頁面文件(列表顯示留言)
#001 <%@ Page language="c#" Codebehind="ListLWord.aspx.cs" AutoEventWireup="false"
Inherits="TraceLWord1.ListLWord" %>
#002 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
#003
#004 <html>
#005 <head>
#006 <title>ListLWord</title>
#007 <meta name="GENERATOR" Content="Microsoft Visual Studio .NET 7.1">
#008 <meta name="CODE_LANGUAGE" Content="C#">
#009 <meta name=vs_defaultClientScript content="JavaScript">
#010 <meta name=vs_targetSchema content="http://schemas.microsoft.com/intellisense/ie5">
#011 </head>
#012 <body MS_POSITIONING="GridLayout">
#013
#014 <form id="__aspNetForm" method="post" runat="server">
#015
#016 <a href="PostLWord.aspx">發送新留言</a>
#017
#018 <asp:DataList ID="m_lwordListCtrl" Runat="Server">
#019 <ItemTemplate>
#020 <div>
#021 <%# DataBinder.Eval(Container.DataItem, "PostTime") %>
#022 <%# DataBinder.Eval(Container.DataItem, "TextContent") %>
#023 </div>
#024 </ItemTemplate>
#025 </asp:DataList>
#026
#027 </form>
#028
#029 </body>
#030 </html>
以最普通的設計方式製作留言板,效率很高。
這些代碼可以在Visual Studio.NET 2003開發環境的設計視圖中快速建立。
ListLWord.aspx 後臺程序文件 ListLWord.aspx.cs
#001 using System;
#002 using System.Collections;
#003 using System.ComponentModel;
#004 using System.Data;
#005 using System.Data.OleDb; // 需要操作 Access 數據庫
#006 using System.Drawing;
#007 using System.Web;
#008 using System.Web.SessionState;
#009 using System.Web.UI;
#010 using System.Web.UI.WebControls;
#011 using System.Web.UI.HtmlControls;
#012
#013 namespace TraceLWord1
#014 {
#015 ///<summary>
#016 /// ListLWord 列表留言板信息
#017 ///</summary>
#018 public class ListLWord : System.Web.UI.Page
#019 {
#020 // 留言列表控件
#021 protected System.Web.UI.WebControls.DataList m_lwordListCtrl;
#022
#023 ///<summary>
#024 /// ListLWord.aspx 頁面加載函數
#025 ///</summary>
#026 private void Page_Load(object sender, System.EventArgs e)
#027 {
#028 LWord_DataBind();
#029 }
#030
#031 #region Web 窗體設計器生成的代碼
#032 override protected void OnInit(EventArgs e)
#033 {
#034 InitializeComponent();
#035 base.OnInit(e);
#036 }
#037
#038 private void InitializeComponent()
#039 {
#040 this.Load+=new System.EventHandler(this.Page_Load);
#041 }
#042 #endregion
#043
#044 ///<summary>
#045 ///綁定留言信息列表
#046 ///</summary>
#047 private void LWord_DataBind()
#048 {
#049 string mdbConn=@"PROVIDER=Microsoft.Jet.OLEDB.4.0;
DATA Source=C:/DbFs/TraceLWordDb.mdb";
#050 string cmdText=@"SELECT * FROM [LWord] ORDER BY [LWordID] DESC";
#051
#052 OleDbConnection dbConn=new OleDbConnection(mdbConn);
#053 OleDbDataAdapter dbAdp=new OleDbDataAdapter(cmdText, dbConn);
#054
#055 DataSet ds=new DataSet();
#056 dbAdp.Fill(ds, @"LWordTable");
#057
#058 m_lwordListCtrl.DataSource=ds.Tables[@"LWordTable"].DefaultView;
#059 m_lwordListCtrl.DataBind();
#060 }
#061 }
#062 }
PostLWord.aspx頁面文件(發送留言到數據庫)
#001 <%@ Page language="c#" Codebehind="PostLWord.aspx.cs" AutoEventWireup="false"
Inherits="TraceLWord1.PostLWord" %>
#002 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
#003
#004 <html>
#005 <head>
#006 <title>PostLWord</title>
#007 <meta name="GENERATOR" Content="Microsoft Visual Studio .NET 7.1">
#008 <meta name="CODE_LANGUAGE" Content="C#">
#009 <meta name=vs_defaultClientScript content="JavaScript">
#010 <meta name=vs_targetSchema content="http://schemas.microsoft.com/intellisense/ie5">
#011 </head>
#012 <body MS_POSITIONING="GridLayout">
#013
#014 <form id="__aspNetForm" method="post" runat="server">
#015
#016 <textarea id="m_txtContent" runat="Server" rows=8 cols=48></textarea>
#017 <input type="Button" id="m_btnPost" runat="Server" value="發送留言" />
#018
#019 </form>
#020
#021 </body>
#022 </html>
PostLWord.aspx後臺程序文件PostLWord.aspx.cs
#001 using System;
#002 using System.Collections;
#003 using System.ComponentModel;
#004 using System.Data;
#005 using System.Data.OleDb; // 需要操作 Access 數據庫
#006 using System.Drawing;
#007 using System.Web;
#008 using System.Web.SessionState;
#009 using System.Web.UI;
#010 using System.Web.UI.WebControls;
#011 using System.Web.UI.HtmlControls;
#012
#013 namespace TraceLWord1
#014 {
#015 ///<summary>
#016 /// PostLWord 發送留言到數據庫
#017 ///</summary>
#018 public class PostLWord : System.Web.UI.Page
#019 {
#020 // 留言內容編輯框
#021 protected System.Web.UI.HtmlControls.HtmlTextArea m_txtContent;
#022 // 提交按鈕
#023 protected System.Web.UI.HtmlControls.HtmlInputButton m_btnPost;
#024
#025 ///<summary>
#026 /// PostLWord.aspx 頁面加載函數
#027 ///</summary>
#028 private void Page_Load(object sender, System.EventArgs e)
#029 {
#030 }
#031
#032 #region Web 窗體設計器生成的代碼
#033 override protected void OnInit(EventArgs e)
#034 {
#035 InitializeComponent();
#036 base.OnInit(e);
#037 }
#038
#039 private void InitializeComponent()
#040 {
#041 this.Load+=new System.EventHandler(this.Page_Load);
#042 this.m_btnPost.ServerClick+=new EventHandler(Post_ServerClick);
#043 }
#044 #endregion
#046 ///<summary>
#047 ///發送留言信息到數據庫
#048 ///</summary>
#049 private void Post_ServerClick(object sender, EventArgs e)
#050 {
#051 // 獲取留言內容
#052 string textContent=this.m_txtContent.Value;
#053
#054 // 留言內容不能爲空
#055 if(textContent=="")
#056 throw new Exception("留言內容爲空");
#057
#058 string mdbConn=@"PROVIDER=Microsoft.Jet.OLEDB.4.0; DATA Source=C:/DbFs/TraceLWordDb.mdb";
#059 string cmdText="INSERT INTO [LWord]([TextContent]) VALUES(@TextContent)";
#060
#061 OleDbConnection dbConn=new OleDbConnection(mdbConn);
#062 OleDbCommand dbCmd=new OleDbCommand(cmdText, dbConn);
#063
#064 // 設置留言內容
#065 dbCmd.Parameters.Add(new OleDbParameter("@TextContent",
OleDbType.LongVarWChar));
#066 dbCmd.Parameters["@TextContent"].Value=textContent;
#067
#068 try
#069 {
#070 dbConn.Open();
#071 dbCmd.ExecuteNonQuery();
#072 }
#073 catch
#074 {
#075 throw;
#076 }
#077 finally
#078 {
#079 dbConn.Close();
#080 }
#081
#082 // 跳轉到留言顯示頁面
#083 Response.Redirect("ListLWord.aspx", true);
#084 }
#085 }
#086 }
僅僅通過兩個頁面,就完成了一個基於Access數據庫的留言功能。
程序並不算複雜,非常簡單清楚。但是隨後你會意識到其存在着不靈活性!
爲什麼需要“三層結構”?——數據庫升遷、應用程序變化所帶來的問題
留言板正式投入使用!但沒過多久,我準備把這個留言板程序的數據庫升遷到Microsoft SQL Server 2000服務器上去!除了要把數據導入到SQL Server 2000中,還得修改相應的.aspx.cs程序文件。也就是說需要把調用OleDbConnection的地方修改成SqlConnection,還要把調用OleDbAdapter的地方,修改成SqlAdapter。雖然這並不是一件很困難的事情,因爲整個站點非常小,僅僅只有兩個程序文件,所以修改起來並不費勁。但是,如果對於一個大型的商業網站,訪問數據庫的頁面有很多很多,如果以此方法一個頁面一個頁面地進行修改,那麼費時又費力!只是修改了一下數據庫,卻可能要修改上千張網頁。一動百動,這也許就是程序的一種不靈活性……
再假如,我想給留言板加一個限制:
n 每天上午09時之後到11時之前可以留言,下午則是13時之後到17時之前可以留言
n 如果當天留言個數小於 40,則可以繼續留言
那麼就需要把相應的代碼,添加到PostLWord.aspx.cs程序文件中。但是過了一段時間,我又希望去除這個限制,那麼還要修改PostLWord.aspx.cs文件。但是,對於一個大型的商業網站,類似於這樣的限制,或者稱爲“商業規則”,複雜又繁瑣。而且這些規則很容易隨着商家的意志爲轉移。如果這些規則限制被分散到各個頁面中,那麼規則一旦變化,就要修改很多的頁面!只是修改了一下規則限制,卻又可能要修改上千張網頁。一動百動,這也許又是程序的一種不靈活性……
最後,留言板使用過一段時間之後,出於某種目的,我希望把它修改成可以在本地運行的Windows程序,而放棄原來的Web型式。那麼對於這個留言板,可以說是“滅頂之災”。所有代碼都要重新寫……當然這個例子比較極端,在現實中,這樣的情況還是很少會發生的——
爲什麼需要“三層結構”?——初探,就從數據庫的升遷開始
一個站點中,訪問數據庫的程序代碼散落在各個頁面中,就像夜空中的星星一樣繁多。這樣一動百動的維護,難度可想而知。最難以忍受的是,對這種維護工作的投入,是沒有任何價值的……
有一個比較好的解決辦法,那就是將訪問數據庫的代碼全部都放在一個程序文件裏。這樣,數據庫平臺一旦發生變化,那麼只需要集中修改這一個文件就可以了。我想有點開發經驗的人,都會想到這一步的。這種“以不變應萬變”的做法其實是簡單的“門面模式”的應用。如果把一個網站比喻成一家大飯店,那麼“門面模式”中的“門面”,就像是飯店的服務生,而一個網站的瀏覽者,就像是一個來賓。來賓只需要發送命令給服務生,然後服務生就會按照命令辦事。至於服務生經歷了多少辛苦才把事情辦成?那個並不是來賓感興趣的事情,來賓們只要求服務生儘快把自己交待事情辦完。我們就把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程序文件,它便是“中間業務層”程序。爲了不重複命名,TraceLWord3的網站被放置到了WebUI項目中。更完整的代碼,可以在CodePackage/TraceLWord3目錄中找到——
這些類的關係,也可以表示爲如下的示意圖:
LWordService.cs程序源碼:
#001 using System;
#002 using System.Data;
#003
#004 using TraceLWord3.AccessTask; // 引用數據訪問層
#005
#006 namespace TraceLWord3.InterService
#007 {
#008 ///<summary>
#009 /// LWordService 留言板服務類
#010 ///</summary>
#011 public class LWordService
#012 {
#013 ///<summary>
#014 ///讀取數據庫表 LWord,並填充 DataSet 數據集
#015 ///</summary>
#016 ///<param name="ds">填充目標數據集</param>
#017 ///<param name="tableName">表名稱</param>
#018 ///<returns>記錄行數</returns>
#019 public int ListLWord(DataSet ds, string tableName)
#020 {
#021 return (new LWordTask()).ListLWord(ds, tableName);
#022 }
#023
#024 ///<summary>
#025 ///發送留言信息到數據庫
#026 ///</summary>
#027 ///<param name="textContent">留言內容</param>
#028 public void PostLWord(string content)
#029 {
#030 (new LWordTask()).PostLWord(content);
#031 }
#032 }
#033 }
從LWordService.cs程序文件的行#021和行#030可以看出,“中間業務層”並沒有實現什麼業務邏輯,只是簡單的調用了“數據訪問層”的類方法……這樣做是爲了讓讀者更直觀的看明白“三層結構”應用程序的調用順序,看清楚它的全貌。加入了“中間業務層”,那麼原來的ListLWord.aspx.cs文件應該作以修改:
...
#012 using TraceLWord3.InterService; // 引用中間服務層
...
#045 ///<summary>
#046 ///綁定留言信息列表
#047 ///</summary>
#048 private void LWord_DataBind()
#049 {
#050 DataSet ds=new DataSet();
#051 (new LWordService()).ListLWord(ds, @"LWordTable");
#052
#053 m_lwordListCtrl.DataSource=ds.Tables[@"LWordTable"].DefaultView;
#054 m_lwordListCtrl.DataBind();
#055 }
...
原來的PostLWord.aspx.cs文件也應作以修改:
...
#012 using TraceLWord3.InterService; // 引用中間服務層
...
#047 ///<summary>
#048 ///發送留言到數據庫
#049 ///</summary>
#050 private void Post_ServerClick(object sender, EventArgs e)
#051 {
#052 // 獲取留言內容
#053 string textContent=this.m_txtContent.Value;
#054
#055 (new LWordService()).PostLWord(textContent);
#056
#057 // 跳轉到留言顯示頁面
#058 Response.Redirect("ListLWord.aspx", true);
#059 }
...
到目前爲止,TraceLWord3程序已經是一個簡單的“三層結構”的應用程序,以一段比較簡單的代碼來描述四個程序的關係:
namespace TraceLWord3.WebLWord
{
public class ListLWord
{
private void LWord_DataBind()
{
(new LWordService()).ListLWord( ... );
}
}
public class PostLWord
{
private void Post_ServerClick(object sender, EventArgs e)
{
(new LWordService()).PostLWord( ... );
}
}
}
namespace TraceLWord3.InterService
{
public class LWordTask
{
public DataSet ListLWord(DataSet ds, string tableName)
{
return (new LWordTask()).ListLWord(ds, tableName);
}
public void PostLWord(string content)
{
(new LWordTask()).PostLWord(content);
}
}
}
namespace TraceLWord3.AccessTask
{
public class LWordTask
{
public DataSet ListLWord(DataSet ds)...
public void PostLWord(string content)...
}
}
用戶在訪問TraceLWord3的ListLWord.aspx頁面時序圖:
當一個用戶訪問TraceLWord5的ListLWord.aspx頁面的時候,會觸發該頁面後臺程序中的Page_Load函數。而在該函數中調用了LWord_DataBind函數來獲取留言板信息。由圖中可以看到出,LWord_DataBind在被調用的期間,會建立一個新的LWordService類對象,並調用這個對象的ListLWord函數。在LWordService.ListLWord函數被調用的期間,會建立一個新的LWordTask類對象,並調用這個對象的ListLWord來獲取留言板信息的。PostLWord.aspx頁面時序圖,和上面這個差不多。就是這樣,經過一層又一層的調用,來獲取返回結果或是保存數據。
注意:從時序圖中可以看出,當子程序模塊未執行結束時,主程序模塊只能處於等待狀態。這說明將應用程序劃分層次,會帶來其執行速度上的一些損失……
對“三層結構”的深入理解——怎樣纔算是一個符合“三層結構”的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”項目中。
如果只以分層的設計角度看,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頁面的後臺程序會去調用中間業務層的相應函數來獲取結果。中間業務層又會去調用數據訪問層的相應函數來獲取結果。在一個用戶訪問TraceLWord3打開ListLWord.aspx頁面查看留言的時候,其後臺程序ListLWord.aspx.cs會去調用位於中間業務層LWordService的ListLWord(DataSet ds)函數。然後這個函數又會去調用位於數據訪問層AccessTask的ListLWord(DataSet ds)函數。最後把結果顯示出來……
對比一下示意圖:
從示意圖看,這兩個過程是否非常相似呢?
不同的地方只是在於,去餐館吃飯,需要吃客自己喚來服務生。而訪問一個asp.net網站,菜單可以代替吃客喚來服務生。在最後的返回結果上,把結果返回給aspx頁面,也就是等於把結果返回給瀏覽者了。
高度的“面向對象思想”的體現——封裝
在我們去餐館吃飯的這個過程中,像我這樣在餐館中的吃客,最關心的是什麼呢?當然是:餐館的飯菜是不是好吃,是不是很衛生?價格是不是公道?……而餐館中的服務生會關心什麼呢?應該是:要隨時注意響應每位顧客的吩咐,要記住顧客在哪個桌位上?還要把顧客點的菜記在本子上……餐館的大廚師傅會關心什麼呢?應該是:一道菜餚的做法是什麼?怎麼提高燒菜的效率?研究新菜式……大廚師傅,燒好菜餚之後,只管把菜交給服務生就完事了。至於服務生把菜送到哪個桌位上去了?是哪個顧客吃了他做的菜,大廚師傅纔不管咧——服務生只要記得把我點的菜餚端來,就成了。至於這菜是怎麼烹飪的?顧客幹麻要點這道菜?他纔不管咧——而我,只要知道這菜味道不錯,價格公道,乾淨衛生,其他的我纔不管咧——
這裏面不正是高度的體現了“面向對象思想”的“封裝”原則嗎?
無論大廚師傅在什麼時候研究出新的菜式,都不會耽誤我現在吃飯。就算服務生忘記我的桌位號是多少了,也不可能因此讓大廚師傅忘記菜餚的做法?在我去餐館吃飯的這個過程中,我、餐館服務生、大廚師傅,是封裝程度極高的三個個體。當其中的一個個體內部發生變化的時候,並不會波及到其他個體。這便是面向對象封裝特性的一個益處!
土豆燉牛肉蓋飯與實體規範
在我工作過的第一家公司樓下,有一家成都風味的小餐館,每天中午我都和幾個同事一起去那家小餐館吃飯。公司附近只有這麼一家餐館,不過那裏的飯菜還算不錯。我最喜歡那裏的“土豆燉牛肉蓋飯”,也很喜歡那裏的“雞蛋湯”,那種美味至今難忘……所謂“蓋飯”,又稱是“蓋澆飯”,就是把烹飪好的菜餚直接遮蓋在鋪在盤子裏的米飯上。例如“土豆燉牛肉蓋飯”,就是把一鍋熱氣騰騰的“土豆燉牛肉”遮蓋在米飯上——
當我和同事再次來到這家餐館吃飯,讓我們想象以下這樣的情形:
情形一:
我對服務生道:給我一份好吃的!
服務生道:什麼好吃的?
我答道:一份好吃的——
三番幾次……
我對服務生大怒道:好吃的,好吃的,你難道不明白嗎?!——
這樣的情況是沒有可能發生的!因爲我沒有明確地說出來我到底要吃什麼?所以服務生也沒辦法爲我服務……
問題後果:我可能被送往附近醫院的精神科……
情形二:
我對服務生道:給我一份土豆燉牛肉蓋飯!
服務生對大廚師傅道:做一份宮爆雞丁——
這樣的情況是沒有可能發生的!因爲我非常明確地說出來我要吃土豆燉牛肉蓋飯!但是服務生卻給我端上了一盤宮爆雞丁?!
問題後果:我會投訴這個服務生的……
情形三:
我對服務生道:給我一份土豆燉牛肉蓋飯!
服務生對大廚師傅道:做一份土豆燉牛肉蓋飯——
大廚師傅道:宮爆雞丁做好了……
這樣的情況是沒有可能發生的!因爲我非常明確地說出來我要吃土豆燉牛肉蓋飯!服務生也很明確地要求大廚師傅做一份土豆燉牛肉蓋飯。但是廚師卻烹製了一盤宮爆雞丁?!
問題後果:我會投訴這家餐館的……
情形四:
我對服務生道:給一份土豆燉牛肉蓋飯!
服務生對大廚師傅道:做一份土豆燉牛肉蓋飯——
大廚師傅道:土豆燉牛肉蓋飯做好了……
服務生把蓋飯端上來,放到我所在的桌位。我看着香噴噴的土豆燉牛肉蓋飯,舉勺下口正要吃的時候,卻突然發現這盤土豆燉牛肉蓋飯變成了石頭?!
這樣的情況更是沒有可能發生的!必定,現實生活不是《西遊記》。必定,這篇文章是學術文章而不是《哈里波特》……
問題後果:……
如果上面這些荒唐的事情都成了現實,那麼我肯定永遠都不敢再來這家餐館吃飯了。這些讓我感到極大的不安。而在TraceLWord3這個項目中呢?似乎上面這些荒唐的事情都成真了。(我想,不僅僅是在TraceLWord3這樣的項目中,作爲這篇文章的讀者,你是否也經歷過像這一樣荒唐的項目而全然未知呢?)
首先在ListLWord.aspx.cs文件
...
#048 private void LWord_DataBind()
#049 {
#050 DataSet ds=new DataSet();
#051 (new LWordService()).ListLWord(ds, @"LWordTable");
#052
#053 m_lwordListCtrl.DataSource=ds.Tables[@"LWordTable"].DefaultView;
#054 m_lwordListCtrl.DataBind();
#055 }
...
在ListLWord.aspx.cs文件中,使用的是DataSet對象來取得留言板信息的。但是DataSet是不明確的!爲什麼這麼說呢?行#051由LWordService填充的DataSet中可以集合任意的數據表DataTable,而在這些被收集的DataTable中,不一定會有一個是我們期望得到的。假設,LWordService類中的ListLWord函數其函數內容是:
...
#006 namespace TraceLWord3.InterService
#007 {
...
#011 public class LWordService
#012 {
...
#019 public int ListLWord(DataSet ds, string tableName)
#020 {
#021 ds.Tables.Clear();
#022 ds.Tables.Add(new DataTable(tableName));
#023
#024 return 1;
#025 }
...
函數中清除了數據集中所有的表之後,加入了一個新的數據表後就匆匆返回了。這樣作的後果,會直接影響ListLWord.aspx。
...
#018 <asp:DataList ID="m_lwordListCtrl" Runat="Server">
#019 <ItemTemplate>
#020 <div> <!--// 會提示找不到下面這兩個字段 //-->
#021 <%# DataBinder.Eval(Container.DataItem, "PostTime") %>
#022 <%# DataBinder.Eval(Container.DataItem, "TextContent") %>
#023 </div>
#024 </ItemTemplate>
#025 </asp:DataList>
...
這和前面提到的“情形一”,一模一樣!我沒有明確地提出自己想要的飯菜,但是餐館服務生卻揣摩我的意思,擅自作主。
其次,再看LWordService.cs文件
...
#019 public int ListLWord(DataSet ds, string tableName)
#020 {
#021 return (new LWordTask()).ListLWord(ds, tableName);
#022 }
...
在LWordService.cs文件中,也是使用DataSet對象來取得留言板信息的。這個DataSet同樣的不明確,含糊不清的指令還在執行……行#021由LWordTask填充的DataSet不一定會含有我們希望得到的表。即便是行#019中的DataSet參數已經明確的定義了每個表的結構,那麼在帶入行#021之後,可能也會變得混淆。例如,LWordTask類中的ListLWord函數其函數內容是:
...
#006 namespace TraceLWord2
#007 {
...
#011 public class LWordTask
#012 {
...
#022 public int ListLWord(DataSet ds, string tableName)
#023 {
#024 ds.Tables.Clear();
#025
#026 // 在SQL語句裏選取了 [RegUser] 表而非 [LWord] 表
#027 string cmdText="SELECT * FROM [RegUser] ORDER BY [RegUserID] DESC";
#028
#029 OleDbConnection dbConn=new OleDbConnection(DB_CONN);
#030 OleDbDataAdapter dbAdp=new OleDbDataAdapter(cmdText, dbConn);
#031
#032 int count=dbAdp.Fill(ds, tableName);
#033
#034 return count;
#035 }
...
函數中清除了數據集中所有的表之後,選取了註冊用戶數據表[RegUser]對DataSet進行填充並返回。也就是說,即便是LWordService.cs文件中行#019中的DataSet參數已經明確的定義了每個表的結構,也可能會出現和前面提到的和“情形三”一樣結果。
最後,再看看LWordTask.cs文件
...
#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 }
...
看到這裏,我感到很是欣慰!我只能說我們的大廚師傅是一個厚道的人,而且還是很知我心的人。
我們不能只坐在那裏期盼着我們的程序會往好的方向發展,這樣很被動。寫出上面的這些程序段,必須小心翼翼。就連數據庫表中的字段命名都要一審再審。一旦變化,就直接影響到位於“表現層”的ListLWord.aspx文件。僅僅是爲了順利的完成TraceLWord3這個“大型項目”,頁面設計師要和程序員還有數據庫管理員要進行額外的溝通。我們需要一個“土豆燉牛肉蓋飯”式的強制標準!——
引入實體規範
爲了達到一種“土豆燉牛肉蓋飯”式的強制標準,所以在TraceLWord4中,引入了Classes項目。在這個項目裏,只有一個LWord.cs程序文件。這是一個非常重要的文件,它屬於“實體規範層”,如果是在一個Java項目中,Classes可以看作是:“實體Bean”。更完整的代碼,可以在CodePackage/TraceLWord4目錄中找到——
LWord.cs文件內容如下:
#001 using System;
#002
#003 namespace TraceLWord4.Classes
#004 {
#005 ///<summary>
#006 /// LWord 留言板類定義
#007 ///</summary>
#008 public class LWord
#009 {
#010 // 編號
#011 private int m_uniqueID;
#012 // 文本內容
#013 private string m_textContent;
#014 // 發送時間
#015 private DateTime m_postTime;
#016
#017 #region類 LWord 構造器
#018 ///<summary>
#019 ///類 LWord 默認構造器
#020 ///</summary>
#021 public LWord()
#022 {
#023 }
#024
#025 ///<summary>
#026 ///類 LWord 參數構造器
#027 ///</summary>
#028 ///<param name="uniqueID">留言編號</param>
#029 public LWord(int uniqueID)
#030 {
#031 this.UniqueID=uniqueID;
#032 }
#033 #endregion
#034
#035 ///<summary>
#036 ///設置或獲取留言編號
#037 ///</summary>
#038 public int UniqueID
#039 {
#040 set
#041 {
#042 this.m_uniqueID=(value<=0 ? 0 : value);
#043 }
#044
#045 get
#046 {
#047 return this.m_uniqueID;
#048 }
#049 }
#050
#051 ///<summary>
#052 ///設置或獲取留言內容
#053 ///</summary>
#054 public string TextContent
#055 {
#056 set
#057 {
#058 this.m_textContent=value;
#059 }
#060
#061 get
#062 {
#063 return this.m_textContent;
#064 }
#065 }
#066
#067 ///<summary>
#068 ///設置或獲取發送時間
#069 ///</summary>
#070 public DateTime PostTime
#071 {
#072 set
#073 {
#074 this.m_postTime=value;
#075 }
#076
#077 get
#078 {
#079 return this.m_postTime;
#080 }
#081 }
#082 }
#083 }
這個強制標準,LWordService和LWordTask都必須遵守!所以LWordService相應的要做出變化:
#001 using System;
#002 using System.Data;
#003
#004 using TraceLWord4.AccessTask; // 引用數據訪問層
#005 using TraceLWord4.Classes; // 引用實體規範層
#006
#007 namespace TraceLWord4.InterService
#008 {
#009 ///<summary>
#010 /// LWordService 留言板服務類
#011 ///</summary>
#012 public class LWordService
#013 {
#014 ///<summary>
#015 ///讀取 LWord 數據表,返回留言對象數組
#016 ///</summary>
#017 ///<returns></returns>
#018 public LWord[] ListLWord()
#019 {
#020 return (new LWordTask()).ListLWord();
#021 }
#022
#023 ///<summary>
#024 ///發送留言信息到數據庫
#025 ///</summary>
#026 ///<param name="newLWord">留言對象</param>
#027 public void PostLWord(LWord newLWord)
#028 {
#029 (new LWordTask()).PostLWord(newLWord);
#030 }
#031 }
#032 }
從行#018中可以看出,無論如何,ListLWord函數都要返回一個LWord數組!這個數組可能爲空值,但是一旦數組的長度不爲零,那麼其中的元素必定是一個LWord類對象!而一個LWord類對象,就一定有TextContent和PostTime這兩個屬性!這個要比DataSet類對象作爲參數的形式明確得多……同樣的,LWordTask也要做出反應:
#001 using System;
#002 using System.Collections;
#003 using System.Data;
#004 using System.Data.OleDb;
#005 using System.Web;
#006
#007 using TraceLWord4.Classes; // 引用實體規範層
#008
#009 namespace TraceLWord4.AccessTask
#010 {
#011 ///<summary>
#012 /// LWordTask 留言板任務類
#013 ///</summary>
#014 public class LWordTask
#015 {
#016 // 數據庫連接字符串
#017 private const string DB_CONN=@"PROVIDER=Microsoft.Jet.OLEDB.4.0;
DATA Source=C:/DbFs/TraceLWordDb.mdb";
#018
#019 ///<summary>
#020 ///讀取 LWord 數據表,返回留言對象數組
#021 ///</summary>
#022 ///<returns></returns>
#023 public LWord[] ListLWord()
#024 {
#025 // 留言對象集合
#026 ArrayList lwordList=new ArrayList();
#027
#028 string cmdText="SELECT * FROM [LWord] ORDER BY [LWordID] DESC";
#029
#030 OleDbConnection dbConn=new OleDbConnection(DB_CONN);
#031 OleDbCommand dbCmd=new OleDbCommand(cmdText, dbConn);
#032
#033 try
#034 {
#035 dbConn.Open();
#036 OleDbDataReader dr=dbCmd.ExecuteReader();
#037
#038 while(dr.Read())
#039 {
#040 LWord lword=new LWord();
#041
#042 // 設置留言編號
#043 lword.UniqueID=(int)dr["LWordID"];
#044 // 留言內容
#045 lword.TextContent=(string)dr["TextContent"];
#046 // 發送時間
#047 lword.PostTime=(DateTime)dr["PostTime"];
#048
#049 // 加入留言對象到集合
#050 lwordList.Add(lword);
#051 }
#052 }
#053 catch
#054 {
注意這裏,爲了保證語義明確,使用了一步強制轉型。
而不是直接返回ArrayList對象
|
#055 throw;
#056 }
#057 finally
#058 {
#059 dbConn.Close();
#060 }
#061
#062 // 將集合轉型爲數組並返回給調用者
#063 return (LWord[])lwordList.ToArray(typeof(TraceLWord4.Classes.LWord));
#064 }
#065
#066 ///<summary>
#067 ///發送留言信息到數據庫
#068 ///</summary>
#069 ///<param name="newLWord">留言對象</param>
#070 public void PostLWord(LWord newLWord)
#071 {
#072 // 留言內容不能爲空
#073 if(newLWord==null || newLWord.TextContent==null || newLWord.TextContent=="")
#074 throw new Exception("留言內容爲空");
#075
#076 string cmdText="INSERT INTO [LWord]([TextContent]) VALUES(@TextContent)";
#077
#078 OleDbConnection dbConn=new OleDbConnection(DB_CONN);
#079 OleDbCommand dbCmd=new OleDbCommand(cmdText, dbConn);
#080
#081 // 設置留言內容
#082 dbCmd.Parameters.Add(new OleDbParameter("@TextContent",
OleDbType.LongVarWChar));
#083 dbCmd.Parameters["@TextContent"].Value=newLWord.TextContent;
#084
#085 try
#086 {
#087 dbConn.Open();
#088 dbCmd.ExecuteNonQuery();
#089 }
#090 catch
#091 {
#092 throw;
#093 }
#094 finally
#095 {
#096 dbConn.Close();
#097 }
#098 }
#099 }
#100 }
這樣,即便是將LWordTask.cs文件中的ListLWords方法修改成訪問[RegUser]數據表的代碼,也依然不會影響到外觀層。因爲函數只返回一個LWord類型的數組。再有,因爲位於外觀層的重複器控件綁定的是LWord類對象,而LWord類中就必有對TextContent字段的定義。這樣也就達到了規範數據訪問層返回結果的目的。這便是爲什麼在Duwamish7中會出現Common項目的原因。不知道你現在看明白了麼?而Bincess.CN的做法和PetShop3.0一樣,是通過自定義類來達到實體規範層的目的!PetShop3.0是通過Modal項目,而Bincess.CN則是通過Classes項目。
餐館又來了一位新大廚師傅——談談跨越數據庫平臺的問題
餐館面積不大,但生意很火。每天吃飯的人都特別多。爲了加快上菜的速度,所以餐館又找來了一位新的大廚師傅。假如,TraceLWord4爲了滿足一部分用戶對性能的較高需要,要其數據庫能使用MS SQL Server 2000。那麼我們該怎麼辦呢?數據庫要從Access 2000升遷到MS SqlServer 2000,那麼只要集中修改AccessTask項目中的程序文件就可以了。但是,我又不想讓這樣經典的留言板失去對Access 2000數據庫的支持。所以,正確的做法就是把原來所有的程序完整的拷貝一份放到另外的一個目錄裏。然後集中修改AccessTask項目,使之可以支持MS SQL Server 2000。這樣這個留言板就有了兩個版本,一個是Access 2000版本,另外一個就是MS SQL Server 2000版本……新的大廚師傅過來幫忙了,我們有必要讓原來表現極佳的大廚師傅下課嗎?可這樣,新大廚師傅不是等於沒來一樣?新的大廚師傅過來幫忙了,我們有必要爲新來的大廚師傅重新配備一套餐館服務生系統、菜單系統嗎?當然也沒必要!那麼,可不可以讓TraceLWord4同時支持Access 2000又支持MS SQL Server 2000呢?也就是說,不用完整拷貝原來的程序,而是在解決方案里加入一個新的項目,這個項目存放的是可以訪問MS SQL Server 2000數據庫的代碼。然後,我們再通過一個“開關”來進行控制,當開關指向Access 2000一端時,TraceLWord4就可以運行在Access 2000數據庫平臺上,而如果開關指向MS SQL Server 2000那一端時,TraceLWord4就運行在MS SQL Server 2000數據庫平臺上……
在TraceLWord5中,加入了一個新項目SqlServerTask,這個項目的代碼是訪問的MS SQL Server 2000數據庫。還有一個新建的項目DALFactory,這個項目就是一個“開關”。這個“開關”項目中僅有一個DbTaskDriver.cs程序文件,就是用它來控制TraceLWord5到底運行載那個數據庫平臺上?
關於TraceLWord5,更完整的代碼,可以在CodePackage/TraceLWord5目錄中找到——
DALFactory項目,其實就是“數據訪問層工廠”,而DbTaskDriver類就是一個工廠類。也就是說DALFactory項目是“工廠模式”的一種應用。關於“工廠模式”,顧名思義,工廠是製造產品的地方,而“工廠模式”,就是通過“工廠類”來製造對象實例。“工廠類”可以通過給定的條件,動態地製造不同的對象實例。就好像下面這個樣子:
// 水果基類
public class Fruit;
// 蘋果是一種水果
public class Apple : Fruit;
// 句子是一種水果
public class Orange : Fruit;
|
// 水果工廠類
public class FruitFactory
{
// 根據水果名稱制造一個水果對象
public static Fruit CreateInstance(string fruitName)
{
if(fruitName=="APPLE")
return new Apple();
else if(fruiteName=="ORANGE")
return new Orange();
else
return null;
}
}
|
// 製造一個Apple對象,即:new Apple();
Apple anApple=(Apple)FruitFactory.CreateInstance("APPLE");
// 製造一個Orange對象,即:new Orange();
Orange anOrange=(Orange)FruitFactory.CreateInstance("ORANGE");
工廠類製造對象實例,實際通常是要通過語言所提供的RTTI(RunTime Type Identification運行時類型識別)機制來實現。在Visual C#.NET中,是通過“反射”來實現的。它被封裝在“System.Reflection”名稱空間下,通過C#反射,我們可以在程序運行期間動態地建立對象。關於C#.NET反射,你可以到其它網站上搜索一下相關資料,這裏就不詳述了。左邊是工廠模式的UML示意圖。
新建的DbTaskDriver.cs文件,位於DALFactory項目中
#001 using System;
#002 using System.Configuration;
#003 using System.Reflection; // 需要使用 .NET 反射
#004
#005 namespace TraceLWord5.DALFactory
#006 {
#007 ///<summary>
#008 /// DbTaskDriver 數據庫訪問層工廠
#009 ///</summary>
#010 public class DbTaskDriver
#011 {
#012 類 DbTaskDriver 構造器
#020
#021 ///<summary>
#022 ///驅動數據庫任務對象實例
#023 ///</summary>
#024 public object DriveLWordTask()
#025 {
#026 // 獲取程序集名稱
#027 string assemblyName=ConfigurationSettings.AppSettings["AssemblyName"];
#028 // 獲取默認構造器名稱
#029 string constructor=ConfigurationSettings.AppSettings["Constructor"];
#030
#031 // 建立 AccessTask 或者 SqlServerTask 對象實例
#032 return Assembly.Load(assemblyName).CreateInstance(constructor, false);
#033 }
#034 }
#035 }
那麼相應的,LWordService.cs程序文件也要做相應的修改。
#001 using System;
#002 using System.Data;
#003
#004 using TraceLWord5.AccessTask;
#005 using TraceLWord5.Classes; // 引用實體規範層
#006 using TraceLWord5.DALFactory; // 引用數據訪問層工廠
#007 using TraceLWord5.SqlServerTask;
#008
#009 namespace TraceLWord5.InterService
#010 {
...
#014 public class LWordService
#015 {
...
#020 public LWord[] ListLWord()
#021 {
#022 object dbTask=(new DbTaskDriver()).DriveLWordTask();
#023
#024 // 留言板運行在 Access 數據庫平臺上
#025 if(dbTask is AccessTask.LWordTask)
#026 return ((AccessTask.LWordTask)dbTask).ListLWord();
#027
#028 // 留言板運行在 MS SQL Server 數據庫平臺上
#029 if(dbTask is SqlServerTask.LWordTask)
#030 return ((SqlServerTask.LWordTask)dbTask).GetLWords();
#031
#032 return null;
#033 }
...
#039 public void PostLWord(LWord newLWord)
#040 {
#041 object dbTask=(new DbTaskDriver()).DriveLWordTask();
#042
#043 // 留言板運行在 Access 數據庫平臺上
#044 if(dbTask is AccessTask.LWordTask)
#045 ((AccessTask.LWordTask)dbTask).PostLWord(newLWord);
#046
#047 // 留言板運行在 MS SQL Server 數據庫平臺上
#048 if(dbTask is SqlServerTask.LWordTask)
#049 ((SqlServerTask.LWordTask)dbTask).AddNewLWord(newLWord);
#050 }
#051 }
#052 }
原來的AccessTask項目及程序文件不需要變化,只是多加了一個SqlServerTask項目。新項目中,也有一個LWordTask.cs程序文件,其內容是:
#001 using System;
#002 using System.Collections;
#003 using System.Data;
#004 using System.Data.SqlClient; // 需要訪問 MS SQL Server 數據庫
#005 using System.Web;
#006
#007 using TraceLWord5.Classes; // 引用實體規範層
#008
#009 namespace TraceLWord5.SqlServerTask
#010 {
#011 ///<summary>
#012 /// LWordTask 留言板任務類
#013 ///</summary>
#014 public class LWordTask
#015 {
#016 // 數據庫連接字符串
#017 private const string DB_CONN=@"Server=127.0.0.1; uid=sa; pwd=;
DataBase=TraceLWordDb";
#018
#019 ///<summary>
#020 ///讀取 LWord 數據表,返回留言對象數組
#021 ///</summary>
#022 ///<returns></returns>
#023 public LWord[] GetLWords()
#024 {
#025 // 留言對象集合
#026 ArrayList lwordList=new ArrayList();
#027
#028 string cmdText="SELECT * FROM [LWord] ORDER BY [LWordID] DESC";
#029
#030 SqlConnection dbConn=new SqlConnection(DB_CONN);
#031 SqlCommand dbCmd=new SqlCommand(cmdText, dbConn);
#032
#033 try
#034 {
#035 dbConn.Open();
#036 SqlDataReader dr=dbCmd.ExecuteReader();
#037
#038 while(dr.Read())
#039 {
#040 LWord lword=new LWord();
#041
#042 // 設置留言編號
#043 lword.UniqueID=(int)dr["LWordID"];
#044 // 留言內容
#045 lword.TextContent=(string)dr["TextContent"];
#046 // 發送時間
#047 lword.PostTime=(DateTime)dr["PostTime"];
#048
#049 // 加入留言對象到集合
#050 lwordList.Add(lword);
#051 }
#052 }
#053 catch
#054 {
#055 throw;
#056 }
#057 finally
#058 {
#059 dbConn.Close();
#060 }
#061
#062 // 將集合轉型爲數組並返回給調用者
#063 return (LWord[])lwordList.ToArray(typeof(TraceLWord5.Classes.LWord));
#064 }
#065
#066 ///<summary>
#067 ///發送留言信息到數據庫
#068 ///</summary>
#069 ///<param name="newLWord">留言對象</param>
#070 public void AddNewLWord(LWord newLWord)
#071 {
#072 // 留言內容不能爲空
#073 if(newLWord==null || newLWord.TextContent==null || newLWord.TextContent=="")
#074 throw new Exception("留言內容爲空");
#075
#076 string cmdText="INSERT INTO [LWord]([TextContent]) VALUES(@TextContent)";
#077
#078 SqlConnection dbConn=new SqlConnection(DB_CONN);
#079 SqlCommand dbCmd=new SqlCommand(cmdText, dbConn);
#080
#081 // 設置留言內容
#082 dbCmd.Parameters.Add(new SqlParameter("@TextContent", SqlDbType.NText));
#083 dbCmd.Parameters["@TextContent"].Value=newLWord.TextContent;
#084
#085 try
#086 {
#087 dbConn.Open();
#088 dbCmd.ExecuteNonQuery();
#089 }
#090 catch
#091 {
#092 throw;
#093 }
#094 finally
#095 {
#096 dbConn.Close();
#097 }
#098 }
#099 }
#100 }
特別指出的是,這個SqlServerTask中的LWordTask程序文件,也遵循“土豆燉牛肉蓋飯”式的強制標準!
在TraceLWord5中,也需要配置Web.Config文件,需要加入自定義的鍵值:
#001 <?xmlversion="1.0"encoding="utf-8"?>
#002 <configuration>
#003
#004 <system.web>
#005 <identityimpersonate="true"/>
#006 <compilationdefaultLanguage="c#"debug="true"/>
#007 <customErrorsmode="RemoteOnly"/>
#008 </system.web>
#009
#010 <appSettings>
...
#026 <!--// SQLServer 2000 數據庫任務程序集及驅動類名稱 //-->
#027 <addkey="AssemblyName"
#028 value="TraceLWord5.SqlServerTask"/>
#029 <addkey="Constructor"
#030 value="TraceLWord5.SqlServerTask.LWordTask"/>
#031
#032 </appSettings>
#033
#034 </configuration>
通過修改配置文件中的關鍵信息,就可以修改留言板的數據庫運行平臺。這樣便做到了跨數據庫平臺的目的。
用戶在訪問TraceLWord5的ListLWord.aspx頁面時序圖:
當一個用戶訪問TraceLWord5的ListLWord.aspx頁面的時候,會觸發該頁面後臺程序中的Page_Load函數。而在該函數中調用了LWord_DataBind函數來獲取留言板信息。由圖中可以看到出,LWord_DataBind在被調用的期間,會建立一個新的LWordService類對象,並調用這個對象的ListLWord函數。在LWordService.ListLWord函數被調用的期間,會建立一個新的DALFactory.DbTaskDriver類對象,並調用這個對象的DriveLWordTask函數來建立一個真正的數據訪問層對象。在代碼中,DriveLWordTask函數需要讀取應用程序配置文件。當一個真正的數據訪問層類對象被建立之後,會返給調用者LWordService.ListLWord,調用者會繼續調用這個真正的數據訪問層類對象的GetLWords函數,最終取到留言板數據。PostLWord.aspx頁面時序圖,和上面這個差不多。就是這樣,經過一層又一層的調用,來獲取返回結果或是保存數據。
注意:從時序圖中可以看出,當子程序模塊未執行結束時,主程序模塊只能處於等待狀態。這說明將應用程序劃分層次,會帶來其執行速度上的一些損失……
烹製土豆燒牛肉蓋飯的方法論
TraceLWord5已經實現了跨數據庫平臺的目的。但是稍微細心一點就不難發現,TraceLWord5有一個很致命的缺點。那就是如果要加入對新的數據庫平臺的支持,除去必要的新建數據訪問層項目以外,還要在中間業務層InsetService項目中添加相應的依賴關係和代碼。例如,新加入了對Oracle9i的數據庫支持,那麼除去要新建一個OracleTask項目以外,還要在LWordService中添加對OracleTask項目的依賴關係,並增加代碼如下:
...
#020 public LWord[] ListLWord()
#021 {
#022 object dbTask=(new DbTaskDriver()).DriveLWordTask();
#023
#024 // 留言板運行在 Access 數據庫平臺上
#025 if(dbTask is AccessTask.LWordTask)
#026 return ((AccessTask.LWordTask)dbTask).ListLWord();
#027
#028 // 留言板運行在 MS SQL Server 數據庫平臺上
#029 if(dbTask is SqlServerTask.LWordTask)
#030 return ((SqlServerTask.LWordTask)dbTask).GetLWords();
#031
#032 // 留言板運行在 Oracle 數據庫平臺上
#033 if(dbTask is OracleTask.LWordTask)
#034 return ((OracleTask.LWordTask)dbTask).FetchLWords();
#035
#036 return null;
#037 }
#038
...
每加入對新數據庫的支持,就要修改中間業務層,這是件很麻煩的事情。再有就是,這三個數據訪問層,獲取留言板信息的方法似乎是各自爲政,沒有統一的標準。在AccessTask項目中使用的是ListLWord函數來獲取留言信息;而在SqlServerTask項目中則是使用GetLWords函數來獲取;再到了OracleTask又是換成了FetchLWords……
餐館服務生也許會對新來的大廚師傅很感興趣,或許也會對新來的大廚師傅的手藝很感興趣。但是這些餐館服務生,絕對不會去背誦哪位大廚師傅會做什麼樣的菜,哪位大廚師傅不會做什麼樣的菜?也不會去在意同樣的一道菜餚,兩位大廚師傅不同的烹製步驟是什麼?對於我所點的“土豆燉牛肉蓋飯”,餐館服務生只管對着廚房大聲叫道:“土豆燉牛蓋飯一份!”,飯菜馬上就會做好。至於是哪個廚師做出來的,服務生並不會關心。其實服務生的意思是說:“外面有個顧客要吃‘土豆燉牛肉蓋飯’,你們兩個大廚師傅,哪位會做這個,馬上給做一份……”。如果新來的大廚師傅不會做,那麼原來的大廚師傅會擔起此重任。如果新來的大廚師傅會做,那麼兩個大廚師傅之間誰現在更悠閒一些就由誰來做。
在TraceLWord5中,兩個數據訪問層,都可以獲取和保存留言信息,只是他們各自的函數名稱不一樣。但是對於中間業務層,卻必須詳細的記錄這些,這似乎顯得有些多餘。僅僅是爲了順利的完成TraceLWord5這個“大型項目”,負責中間業務層的程序員要和負責數據訪問層的程序員進行額外的溝通。TraceLWord5中,一個真正的數據訪問層對象實例,是由DALFactory名稱空間中的DbTaskDriver類製造的。如果中間業務層只需要知道“這個真正的數據訪問層對象實例”有能力獲取留言板和存儲留言板,而不用關心其內部實現,那麼就不會隨着數據訪問層項目的增加,而修改中間業務層了。換句直白的話來說就是:如果所有的數據訪問層對象實例,都提供統一的函數名稱“ListLWord函數”和“PostLWord函數”,那麼中間業務層就不需要判斷再調用了。我們需要“烹製土豆燒牛肉蓋飯的方法論”的統一!——
烹製土豆燉牛肉蓋飯方法論的統一——接口實現
怎麼實現“烹製土豆燒牛肉蓋飯方法論”的統一呢?答案是應用接口。在TraceLWord6中,新建了一個DbTask項目,裏面只有一個ILWordTask.cs程序文件,在這裏定義了一個接口。DbTask項目應該屬於“抽象的數據訪問層”。更完整的代碼,可以在CodePackage/TraceLWord6目錄中找到——
DbTask項目中的ILWordTask.cs內容如下:
#001 using System;
#002
#003 using TraceLWord6.Classes; // 引用實體規範層
#004
#005 namespace TraceLWord6.DbTask
#006 {
...
#010 public interface ILWordTask
#011 {
#012 // 獲取留言信息
#013 LWord[] ListLWord();
#014
#015 // 發送新留言信息到數據庫
#016 void PostLWord(LWord newLWord);
#017 }
#018 }
AccessTask項目中的LWordTask.cs需要做出修改:
...
#007 using TraceLWord6.Classes; // 引用實體規範層
#008 using TraceLWord6.DbTask; // 引用抽象的數據訪問層
#009
#010 namespace TraceLWord6.AccessTask
#011 {
...
#015 public class LWordTask : ILWordTask // 實現了ILWordTask接口
#016 {
...
#024 public LWord[] ListLWord()...
...
#071 public void PostLWord(LWord newLWord)...
...
#099 }
#100 }
SqlServerTask項目中的LWordTask.cs需要做出修改:
...
#007 using TraceLWord6.Classes; // 引用實體規範層
#008 using TraceLWord6.DbTask; // 引用抽象的數據訪問層
#009
#010 namespace TraceLWord6.SqlServerTask
#011 {
...
#015 public class LWordTask : ILWordTask // 實現了ILWordTask接口
#016 {
...
#024 public LWord[] ListLWord()...
...
#071 public void PostLWord(LWord newLWord)...
...
#100 }
#101 }
AccessTask項目中的LWordTask類實現了ILWordTask接口,那麼就必須覆寫ListLWord和PostLWord這兩個函數。SqlServerTask項目中的LWordTask類也實現了ILWordTask接口,那麼就也必須覆寫ListLWord和PostLWord這兩個函數。這兩個類對共同的接口ILWordTask的實現,使這兩個類得到空前的統一。這對於求根溯源,向上轉型也是很有幫助的。
DALFactory項目中的DbTaskDriver.cs文件也要作以修改:
...
#026 public ILWordTask DriveLWordTask()
#027 {
#028 // 獲取程序集名稱
#029 string assemblyName=ConfigurationSettings.AppSettings["AssemblyName"];
#030 // 獲取默認構造器名稱
#031 string constructor=ConfigurationSettings.AppSettings["Constructor"];
#032
#033 // 建立 ILWordTask 對象實例
#034 return (ILWordTask)Assembly.Load(assemblyName).CreateInstance(constructor,
false);
...
因爲AccessTask項目中的LWordTask類和SqlServerTask項目中的LWordTask類,都實現了ILWordTask接口。那麼,像行#034這樣的轉型是絕對成立的。而且轉型後的對象,一定含有ListLWord和PostLWord這兩個函數。InterService項目中的LWordService.cs程序文件應該作以修改,中間業務層只依賴於一個抽象的數據訪問層。這樣,修改具體的數據訪問層就不會影響到它了:
...
#008 namespace TraceLWord6.InterService
#009 {
...
#013 public class LWordService
#014 {
#015 ///<summary>
#016 ///讀取 LWord 數據表,返回留言對象數組
#017 ///</summary>
#018 ///<returns></returns>
#019 public LWord[] ListLWord()
#020 {
#021 return (new DbTaskDriver()).DriveLWordTask().ListLWord();
#022 }
#023
#024 ///<summary>
#025 ///發送留言信息到數據庫
#026 ///</summary>
#027 ///<param name="newLWord">留言對象</param>
#028 public void PostLWord(LWord newLWord)
#029 {
#030 (new DbTaskDriver()).DriveLWordTask().PostLWord(newLWord);
#031 }
#032 }
#033 }
一次完整愉快的旅行
就讓我們以ListLWord.aspx頁面開始,進行一次完整愉快的旅行,看清TraceLWord6的運行全過程。當用瀏覽ListLWord.aspx頁面時,服務器首先會調用ListLWord.aspx.cs文件:
...
#021 // 留言列表控件
#022 protected System.Web.UI.WebControls.DataList m_lwordListCtrl;
#023
#024 ///<summary>
#025 /// ListLWord.aspx 頁面加載函數
#026 ///</summary>
#027 private void Page_Load(object sender, System.EventArgs e)
#028 {
#029 LWord_DataBind();
#030 }
...
#045 ///<summary>
#046 ///綁定留言信息列表
#047 ///</summary>
#048 private void LWord_DataBind()
#049 {
#050 m_lwordListCtrl.DataSource=(new LWordService()).ListLWord();
#051 m_lwordListCtrl.DataBind();
#052 }
...
調用InterService名稱空間中的LWordService類
...
#008 namespace TraceLWord6.InterService
#009 {
...
#013 public class LWordService
#016 ///讀取 LWord 數據表,返回留言對象數組
#017 ///</summary>
#018 ///<returns></returns>
#019 public LWord[] ListLWord()
#020 {
#021 return (new DbTaskDriver()).DriveLWordTask().ListLWord();
#022 }
...
#032 }
#033 }
通過數據訪問層工廠來製造對象實例,而工廠類
#001 <?xmlversion="1.0"encoding="utf-8"?>
#002 <configuration>
...
#010 <appSettings>
...
#026 <!--// SQLServer 2000 數據庫任務程序集及驅動類名稱 //-->
#027 <addkey="AssemblyName"
#028 value="TraceLWord6.SqlServerTask"/>
#029 <addkey="Constructor"
#030 value="TraceLWord6.SqlServerTask.LWordTask"/>
#031
#032 </appSettings>
#033
#034 </configuration>
|
DbTaskDriver需要讀取網站應用程序中的:
Web.Config文件。這裏應用了.NET反射機制。
...
#007 namespace TraceLWord6.DALFactory
#008 {
...
#012 public class DbTaskDriver
#013 {
...
#023 ///<summary>
#024 ///驅動數據庫任務對象實例
#025 ///</summary>
#026 public ILWordTask DriveLWordTask()
#027 {
#028 // 獲取程序集名稱
#029 string assemblyName=ConfigurationSettings.AppSettings["AssemblyName"];
#030 // 獲取默認構造器名稱
#031 string constructor=ConfigurationSettings.AppSettings["Constructor"];
#032
#033 // 建立 ILWordTask 對象實例
#034 return (ILWordTask)Assembly.Load(assemblyName).CreateInstance(constructor,
false);
#035 }
#036 }
#037 }
根據配置文件,製造TraceLWord6.SqlServerTask.LWordTask對象
...
#010 namespace TraceLWord6.SqlServerTask
#011 {
...
#015 public class LWordTask : ILWordTask
#016 {
...
#020 ///<summary>
#021 ///讀取 LWord 數據表,返回留言對象數組
#022 ///</summary>
#023 ///<returns></returns>
#024 public LWord[] ListLWord()...
...
#100 }
#101 }
最後按照頁面上的代碼樣式綁定數據:
...
#018 <asp:DataList ID="m_lwordListCtrl" Runat="Server">
#019 <ItemTemplate>
#020 <div>
#021 <%# DataBinder.Eval(Container.DataItem, "PostTime") %>
#022 <%# DataBinder.Eval(Container.DataItem, "TextContent") %>
#023 </div>
#024 </ItemTemplate>
#025 </asp:DataList>
...
至此爲止,一個簡單的“三層結構”Web應用程序的執行全過程已經盡顯在你眼前。執行順序其實並不複雜。
加入商業規則
“商業規則”,是商業活動中的特殊規則。例如:我們去一家超市買東西,這家超市規定:凡是一次消費金額在2000元以上的顧客,可以獲得一張會員卡。憑藉這張會員卡,下次消費可以獲得積分和享受9折優惠。“商業規則”主旨思想是在表達事與事之間,或者是物與物之間,再或者是事與物之間的關係,而不是事情本身或物質本身的完整性。再例如:一個用戶在一個論壇進行新用戶註冊,該論壇系統規定,新註冊的用戶必須在4個小時之後纔可以發送主題和回覆主題。4個小時之內只能瀏覽主題。這也可以視爲一種商業規則。但是,例如:電子郵件地址必須含有“@”字符;用戶暱稱必須是由中文漢字、英文字母、數字或下劃線組成,這些都並不屬於商業規則,這些應該被劃作“實體規則”。它所描述的是物質本身的完整性。
在TraceLWord7中,商業規則是由Rules項目來實現的。其具體的商業規則是:
n 每天上午09時之後到11時之前可以留言,下午則是13時之後到17時之前可以留言
n 如果當天留言個數小於 40,則可以繼續留言
這兩個條件必須同時滿足。更完整的代碼,可以在CodePackage/TraceLWord7目錄中找到——
那麼,商業規則層和中間業務層有什麼區別嗎?其實本質上沒有太大的區別,只是所描述的功能不一樣。一個是功能邏輯實現,另外一個則是商業邏輯實現。另外,中間業務層所描述的功能邏輯通常是不會改變的。但是商業邏輯卻會因爲季節、消費者心理、資金費用等諸多因素而一變再變。把易變的部分提取出來是很有必要的。
LWordRules.cs文件內容:
#001 using System;
#002
#003 using TraceLWord7.Classes;
#004 using TraceLWord7.DALFactory;
#005 using TraceLWord7.DbTask;
#006
#007 namespace TraceLWord7.Rules
#008 {
#009 ///<summary>
#010 /// LWordRules 留言規則
#011 ///</summary>
#012 public class LWordRules
#013 {
#014 ///<summary>
#015 ///驗證是否可以發送新留言
#016 ///</summary>
#017 ///<returns></returns>
#018 public static bool CanPostLWord()
#019 {
...
#027 DateTime currTime=DateTime.Now;
#028
#029 // 每天上午 09 時之後到 11 時之前可以留言,
#030 // 下午則是 13 時之後到 17 時之前可以留言
#031 if(currTime.Hour<=8 || (currTime.Hour>=11 && currTime.Hour<=12) || currTime.Hour>=17)
#032 return false;
#033
#034 // 獲取當天的留言個數
#035 LWord[] lwords=(new DbTaskDriver()).DriveLWordTask().ListLWord(
#036 currTime.Date, currTime.Date.AddDays(1));
#037
#038 // 如果當天留言個數小於 40,則可以繼續留言
#039 if(lwords==null || lwords.Length<40)
#040 return true;
#041
#042 return false;
#043 }
#044 }
#045 }
在LWordService.cs文件中,要加入這樣的規則:
#025 ///<summary>
#026 ///發送留言信息到數據庫
#027 ///</summary>
#028 ///<param name="newLWord">留言對象</param>
#029 public void PostLWord(LWord newLWord)
#030 {
#031 if(!LWordRules.CanPostLWord())
#032 throw new Exception("無法發送新留言,您違反了留言規則");
#033
#034 (new DbTaskDriver()).DriveLWordTask().PostLWord(newLWord);
#035 }
在發送留言之前,調用“商業規則層”來驗證當前行爲是否有效?如果無效則會拋出一個異常。
“三層結構”的缺點
有些網友在讀完這篇文章前作之後,對我提出了一些質疑,這提醒我文章至此還沒有提及“三層結構”的缺點。“三層結構”這個詞眼似乎一直都很熱門,究其原因,或許是這種開發模式應用的比較普遍。但是“三層結構”卻並不是百試百靈的“萬靈藥”,它也存在着缺點。下面就來說說它的缺點……
“三層結構”開發模式的一個非常明顯的缺點就是其執行速度不夠快。當然這個“執行速度”是相對於非分層的應用程序來說的。從文中所給出的時序圖來看,也明顯的暴露了這一缺點。TraceLWord1和TraceLWord2沒有分層,直接調用的ADO.NET所提供的類來獲取數據。但是,TraceLWord6確要經過多次調用才能獲取到數據。在子程序模塊程序沒有返回時,主程序模塊只能處於等待狀態。所以在執行速度上,留言板的版本越高,排名卻越靠後。“三層結構”開發模式,不適用於對執行速度要求過於苛刻的系統,例如:在線訂票,在線炒股等等……它比較擅長於商業規則容易變化的系統。
“三層結構”開發模式,入門難度夠高,難於理解和學習。這是對於初學程序設計的人來說的。以這種模式開發出來的軟件,代碼量通常要稍稍多一些。這往往會令初學者淹沒在茫茫的代碼之中。望之生畏,對其產生反感,也是可以理解的……
其實,無論哪一種開發模式或方法,都是有利有弊的。不會存在一種“萬用法”可以解決任何問題。所以“三層結構”這個詞眼也不會是個例外!是否採用這個模式進行系統開發,要作出比較、權衡之後纔可以。切忌濫用——
結束語
談到這裏,文章對“三層結構”的原理和用意已經作了完整的闡述。作爲這篇文章的作者,在心喜之餘也感到寫作技術文章並不是件很輕鬆的事情,特別是第一次寫作像這樣長達40多頁的文章。爲了能使讀者輕鬆閱讀,每字每句都要斟酌再三,唯恐會引起歧義。在這裏要特別感謝一直關注和支持彬月論壇的網友,他們對彬月論壇的喜愛以及對我的支持,是我寫作的巨大動力。當然,在這裏還要感謝自己的父母,在我辭去原來的工作在家中完成彬月論壇的日子裏,他們給了我極大的支持和理解……
希望這篇文章能將你帶到夢想的地方——
AfritXia,01.18/2005
作 者:AfritXia
QQ聯繫:365410315
在csdn 上的討論帖:http://topic.csdn.net/t/20060120/10/4526856.html