1、1浅谈“三层结构”原理与用意2005 年 02 月 28 日,AfritXia 撰写2006 年 12 月 28 日,AfritXia 第一次修改序在刚刚步入“多层结构”Web 应用程序开发的时候,我阅读过几篇关于“ 三层结构开发”的文章。但其多半都是对PetShop3.0 和 Duwamish7 的局部剖析或者是学习笔记。对“三层结构”通体分析的学术文章几乎没有。2005 年 2 月 11 日,Bincess BBS 彬月论坛开始试运行。不久之后,我写了一篇题目为浅谈“三层结构”原理与用意的文章。旧版文章以彬月论坛程序中的部分代码举例,通过全局视角阐述了什么是“三层结构”的开发模式?为什么要
2、这样做?怎样做?而在这篇文章的新作中,配合这篇文章我写了 7 个程序实例(TraceLWord1TraceLWord7 留言板)以帮助读者理解“三层结构”应用程序。这些程序示例可以在随带的 CodePackage 目录中找到对于那些有丰富经验的 Web 应用程序开发人员,他们认为文章写的通俗易懂,很值得一读。可是对于 初学者,特别是没有任何开发经验的人,文章阅读起来就感到非常困难,不知文章所云。甚至有些读者对“三层结构”的认识更模糊了关于“多层结构”开发模式,存在这样一种争议:一部分学者认为“多层结构”与“面向对象的程序设计思想”有着非常紧密的联系。而另外一部分学者却认为二者之间并无直接联系
3、。写作这篇文章并不是要终结这种争议,其行文目的是希望读者能够明白:在使用 进行 Web 应用程序开发时,实现“多层结构”开发模式的方法、原理及用意。要顺利的阅读这篇文章,希望读者能对“面向对象的程序设计思想”有一定深度的认识,最好能懂一些“设计模式”的知识。如果你并不了解前面这些,那么这篇文章可能并不适合你现在阅读。不过,无论这篇文章面对的读者是谁,我都会尽量将文章写好。我希望这篇文章能成为学习“三层结构”设计思想的经典文章!“三层结构”是什么?“三层结构”一词中的“三层”是指:“表现层” 、 “中间业务层” 、 “数据访问层” 。其中: 表 现 层:位于最外层(最上层) ,离用户最近。用于
4、显示数据和接收用户输入的数据,为用户提供一种交互式操作的界面。 中间业务层:负责处理用户输入的信息,或者是将这些信息发送给数据访问层进行保存,或者是调用数据访问层中的函数再次读出这些数据。中间业务层也可以包括一些对“商业逻辑”描述代码在里面。 数据访问层:仅实现对数据的保存和读取操作。数据访问,可以访问数据库系统、二进制文件、文本文档或是 XML 文档。表 现 层中间业务层数据访问层表 现 层中间业务层数据访问层项目依赖方向 数值返回方向对依赖方向的研究将是本文的重点,数值返回方向基本上是没有变化的。为什么需要 “三层结构”?通常的设计方式在一个大型的 Web 应用程序中,如果不分以层次,那么
5、在将来的升级维护中会遇到很大的麻烦。但在这篇文章里我只想以一个简单的留言板程序为示例,说明通常设计方式的不足2功能说明:ListLWord.aspx(后台程序文件 ListLWord.aspx.cs)列表显示数据库中的每条留言。PostLWord.aspx(后台程序文件 PostLWord.aspx.cs)发送留言到数据库。更完整的示例代码,可以到 CodePackage/TraceLWord1 目录中找到。数据库中,仅含有一张数据表,其结构如下:字段名称 数据类型 默认值 备注说明LWordID INT NOT NULL IDENTITY(1, 1) 留言记录编号TextContent NT
6、ext N 留言内容PostTime DateTime GetDate() 留言发送时间,默认值为当前时间ListLWord.aspx 页面文件(列表显示留言)#001 #002 #003 #004 #005 #006 ListLWord#007 #008 #009 #010 #011 #012 #013 #014 #015 #016 发送新留言#017 #018 #019 #020 #021 #022 #023 #024 #025 #026 #027 #028 #029 #030 以最普通的设计方式制作留言板,效率很高。这些代码可以在Visual Studio.NET 2003开发环境的设计
7、视图中快速建立。3ListLWord.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.
8、Web.UI;#010 using System.Web.UI.WebControls;#011 using System.Web.UI.HtmlControls;#012 #013 namespace TraceLWord1#014 #015 / #016 / ListLWord 列表留言板信息#017 / #018 public class ListLWord : System.Web.UI.Page#019 #020 / 留言列表控件#021 protected System.Web.UI.WebControls.DataList m_lwordListCtrl;#022 #023 /
9、#024 / ListLWord.aspx 页面加载函数#025 / #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 InitializeC
10、omponent()#039 #040 this.Load+=new System.EventHandler(this.Page_Load);#041 #042 #endregion#043 4#044 / #045 / 绑定留言信息列表#046 / #047 private void LWord_DataBind()#048 #049 string mdbConn=“PROVIDER=Microsoft.Jet.OLEDB.4.0; DATA Source=C:DbFsTraceLWordDb.mdb“;#050 string cmdText=“SELECT * FROM LWord ORD
11、ER 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_lword
12、ListCtrl.DataBind();#060 #061 #062 PostLWord.aspx 页面文件(发送留言到数据库)#001 #002 #003 #004 #005 #006 PostLWord#007 #008 #009 #010 #011 #012 #013 #014 #015 #016 #017 #018 #019 #020 #021 #022 5PostLWord.aspx 后台程序文件 PostLWord.aspx.cs#001 using System;#002 using System.Collections;#003 using System.ComponentMo
13、del;#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 /
14、#016 / PostLWord 发送留言到数据库#017 / #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 / #026 / PostLWord.aspx 页面加载函数#027 / #028 pr
15、ivate 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
16、_Load);#042 this.m_btnPost.ServerClick+=new EventHandler(Post_ServerClick);#043 #044 #endregion6#046 / #047 / 发送留言信息到数据库#048 / #049 private void Post_ServerClick(object sender, EventArgs e)#050 #051 / 获取留言内容#052 string textContent=this.m_txtContent.Value;#053 #054 / 留言内容不能为空#055 if(textContent=“)#05
17、6 throw new Exception(“留言内容为空“);#057 #058 string mdbConn=“PROVIDER=Microsoft.Jet.OLEDB.4.0; DATA Source=C:DbFsTraceLWordDb.mdb“;#059 string cmdText=“INSERT INTO LWord(TextContent) VALUES(TextContent)“;#060 #061 OleDbConnection dbConn=new OleDbConnection(mdbConn);#062 OleDbCommand dbCmd=new OleDbComm
18、and(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
19、dbConn.Close();#080 #081 #082 / 跳转到留言显示页面#083 Response.Redirect(“ListLWord.aspx“, true);#084 #085 #086 仅仅通过两个页面,就完成了一个基于 Access 数据库的留言功能。程序并不算复杂,非常简单清楚。但是随后你会意识到其存在着不灵活性!7为什么需要“三层结构”?数据库升迁、应用程序变化所带来的问题留言板正式投入使用!但没过多久,我准备把这个留言板程序的数据库升迁到 Microsoft SQL Server 2000 服务器上去!除了要把数据导入到 SQL Server 2000 中,还得修改
20、相应的.aspx.cs 程序文件。也就是说需要把调用 OleDbConnection的地方修改成 SqlConnection,还要把调用 OleDbAdapter 的地方,修改成 SqlAdapter。虽然这并不是一件很困难的事情,因为整个站点非常小,仅仅只有两个程序文件,所以修改起来并不费劲。但是,如果对于一个大型的商业网站,访问数据库的页面有很多很多,如果以此方法一个页面一个页面地进行修改,那么费时又费力!只是修改了一下数据库,却可能要修改上千张网页。一动百动,这也许就是程序的一种不灵活性再假如,我想给留言板加一个限制: 每天上午09时之后到11 时之前可以留言,下午则是13时之后到17时
21、之前可以留言 如果当天留言个数小于 40,则可以继续留言那么就需要把相应的代码,添加到 PostLWord.aspx.cs 程序文件中。但是过了一段时间,我又希望去除这个限制,那么还要修改 PostLWord.aspx.cs 文件。但是,对于一个大型的商业网站,类似于这样的限制,或者称为“商业规则” ,复杂又繁琐。而且这些规则很容易随着商家的意志为转移。如果这些规则限制被分散到各个页面中,那么规则一旦变化,就要修改很多的页面!只是修改了一下规则限制,却又可能要修改上千张网页。一动百动,这也许又是程序的一种不灵活性最后,留言板使用过一段时间之后,出于某种目的,我希望把它修改成可以在本地运行的 W
22、indows 程序,而放弃原来的Web 型式。那么对于这个留言板,可以说是“灭顶之灾” 。所有代码都要重新写当然这个例子比较极端,在现实中,这样的情况还是很少会发生的为什么需要“三层结构”?初探,就从数据库的升迁开始一个站点中,访问数据库的程序代码散落在各个页面中,就像夜空中的星星一样繁多。这样一动百动的维护,难度可想而知。最难以忍受的是,对这种维护工作的投入,是没有任何价值的有一个比较好的解决办法,那就是将访问数据库的代码全部都放在一个程序文件里。这样,数据库平台一旦发生变化,那么只需要集中修改这一个文件就可以了。我想有点开发经验的人,都会想到这一步的。这种“以不变应万变”的做法其实是简单的
23、“门面模式”的应用。如果把一个网站比喻成一家大饭店,那么“门面模式”中的“门面” ,就像是饭店的服务生,而一个网站的浏览者,就像是一个来宾。来宾只需要发送命令给服务生,然后服务生就会按照命令办事。至于服务生经历了多少辛苦才把事情办成?那个并不是来宾感兴趣的事情,来宾们只要求服务生尽快把自己交待事情办完。我们就把 ListLWord.aspx.cs 程序就看成是一个来宾发出的命令,而把新加入的 LWordTask.cs 程序看成是一个饭店服务生,那么来宾发出的命令就是:“给我读出留言板数据库中的数据,填充到 DataSet 数据集中并显示出来!”而服务生接到命令后,就会依照执行。而 PostLW
24、ord.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 TraceLWord
25、2#007 #008 / #009 / LWordTask 数据库任务类#010 / #011 public class LWordTask#012 8#013 / 数据库连接字符串#014 private const string DB_CONN=“PROVIDER=Microsoft.Jet.OLEDB.4.0; DATA Source=C:DbFsTraceLWordDb.mdb“;#015 #016 / #017 / 读取数据库表 LWord,并填充 DataSet 数据集#018 / #019 / 填充目标数据集#020 / 表名称#021 / 记录行数#022 public int
26、 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
27、count;#032 #033 #034 / #035 / 发送留言信息到数据库#036 / #037 / 留言内容 #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 OleDbConnec
28、tion 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 dbCm
29、d.ExecuteNonQuery();9#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_
30、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 函数被修改成:
31、.#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
32、.cs 这两个文件已经找不到和数据库相关的代码了。只看到一些和 LWordTask 类有关系的代码,这就符合了“设计模式”中的一种重要原则:“迪米特法则” 。 “迪米特法则”主要是说:让一个“类”与尽量少的其它的类发生关系。在 TraceLWord1 中,ListLWord.aspx.cs 这个类和10OleDbConnection 及 OleDbDataAdapter 都发生了关系,所以它破坏了“迪米特法则” 。利用一个“中间人”是“迪米特法则”解决问题的办法,这也是“门面模式”必须遵循的原则。下面就引出这个 LWordTask 门面类的示意图:L i s t L W o r d P o s
33、 t L W o r dO l e D b C o n n e c t i o n O l e D b A d a p t e rL i s t L W o r d P o s t L W o r dL W o r d T a s kO l e D b C o n n e c t i o n O l e D b A d a p t e rListLWord.aspx.cs 和 PostLWord.aspx.cs 两个文件对数据库的访问,全部委托 LWordTask 类这个“中间人”来办理。利用“门面模式” ,将页面类和数据库类进行隔离。这样就作到了页面类不依赖于数据库的效果。以一段比较简单的代
34、码来描述这三个程序的关系:public class ListLWordprivate void LWord_DataBind()(new LWordTask().ListLWord( . );public class PostLWordprivate void Post_ServerClick(object sender, EventArgs e)(new LWordTask().PostLWord( . );public class LWordTaskpublic DataSet ListLWord(DataSet ds).public void PostLWord(string textContent).