对“三层结构”的深入理解——怎样才算是一个符合“三层结构”的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 {
#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项目。
Word教程网 | Excel教程网 | Dreamweaver教程网 | Fireworks教程网 | PPT教程网 | FLASH教程网 | PS教程网 |
HTML教程网 | DIV CSS教程网 | FLASH AS教程网 | ACCESS教程网 | SQL SERVER教程网 | C语言教程网 | JAVASCRIPT教程网 |
ASP教程网 | ASP.NET教程网 | CorelDraw教程网 |