浅谈“三层结构”的原理与用意对于有经验的Web应用程序开发人员来说,“三层结构”一词应该不会感到陌生。
其实“三层结构”的开发模式不仅仅可以应用于Web应用程序,在其他应用领域也是可以发挥其巨大作用的。
而本文主旨是阐明三层结构的原理与用意,并说明Bincess的三层结构的特点。
“三层结构”指的是什么?“三层结构”一词中的“三层”是指:“外观层”、“中间层”、“数据库层”。
其中:☐外观层:位于最外层,直接呈现在用户面前。
用于显示数据,并为用户提供一种交互式的界面。
☐中间层:负责处理用户输入的信息,或者是将这些信息发送给数据库层进行保存,或者是调用数据库层中的函数再次读出这些数据。
☐数据库层:仅实现对数据的保存和读取操作。
为什么需要“三层结构”在一个软件系统中,如果不分以层次,那么在将来的升级维护中会遇到很大的麻烦。
就像一个网页访问数据库一样。
例如在后台程序文件aspx.cs中,使用OleDbConnection和OleDbCommand来处理Access 后台数据库。
而当数据库服务器从Access2000升迁到SQLServer2000的时候,我们就必须修改原来的OleDbConnection为新的SqlConnection,OleDbCommand为新的SqlCommand来适应新的数据库服务器。
但问题是对于一个大型的商业网站,要进行数据库操作的并不只有一两个页面。
访问数据库的代码会散落各个页面中,就像夜空中的星星一样。
这样的维护,难度可想而知。
有一个比较好的解决办法,那就是将访问数据库的代码全部都放在一个cs文件里,这样数据库服务器一旦变换,那么只需要集中修改一个cs文件就可以了。
namespace Bincess // ListBoard.aspx.cs文件{public class ListBoard{private void BoardDataBind(){OleDbConnection dbConn=new OleDbConnection();OleDbCommand dbCmd=new OleDbCommand();...}}}namespace Bincess // ListTopic.aspx.cs文件{public class ListTopic{private void TopicDataBind(){OleDbConnection dbConn=new OleDbConnection();OleDbCommand dbCmd=new OleDbCommand();...}} 注意,这两个文件都进行了数据库操作。
那么在数据库服务器改换时,两个文件就都必须修改并重新编译…将原来的访问数据库的代码全部都放在DBTask.cs 程序文件中,这样只要修改这一个文件就可以适应新的数据库 namespace Bincess// DBTask.cs {public class DBTask{public void BoardDataBind() {OleDbConnection dbConn=new OleDbConnection();OleDbCommand dbCmd=new OleDbCommand();...}public void TopicDataBind(){OleDbConnection dbConn=new OleDbConnection();OleDbCommand dbCmd=new OleDbCommand();...}} }namespace Bincess// ListBoard.aspx.cs 文件 {public class ListBoard{private void BoardDataBind(){(new DBTask()).BoardDataBind(); }}}namespace Bincess// ListTopic.aspx.cs 文件 {public class ListTopic{private void TopicDataBind(){(new DBTask()).TopicDataBind();}} }当然这是一个简单的“门面模式”的应用,恐怕也是“三层结构”的最原始模型…定义一个DBTask 类,让它来完成所有的数据库操作。
那么当数据库服务器改换时,只要集中修改这一个文件并重新编译即可…怎样才算是一个符合“三层结构”的Web应用程序?在一个 Web应用程序解决方案中,并不是说有aspx文件、有dll文件、还有数据库,就是“三层结构”的Web应用程序,这样的说法是不对的。
也并不是说没有对数据库进行操作,即没有“数据库层”,就不是“三层结构”的。
其实三层结构是功能实现上的三层:☐外观层,用于显示,并为用户提供交互式操作的可能…☐中间层,服务于外观层并调用数据库层的函数。
☐数据库层,实现数据库的存储和读出。
存储目标不一定是数据库服务器,也可以是文本文档或XML文档。
在微软的示范实例Duwamish7中,外观层被放置在Web项目中,中间层是放置在BusinessFacade项目中,而数据库层则是放置在DataAccess项目中。
而微软的另一个示范实例PetShop中,外观层被放置在Web项目中,中间层是放置在BLL项目中,而数据库层则是放置在SQLServerDAL和OracleDAL两个项目中。
在我的彬月论坛中,外观层是被放置在Web项目中,中间层是被放置在InterService项目中,而数据库层是被放置在AccessTask项目中。
显然PetShop要比Duwamish7复杂的多!如果先不讨论这些,那么现在的问题就是:既然三层结构已经被分派到各自的项目中,那么剩下来的项目是做什么的呢?例如PetShop中的Model、IDAL、DALFactory这三个项目,再例如Duwamish7中的Common项目,还有就是在我的论坛中的Classes、DBTask、DALFactory三个项目。
它们是做什么用的呢?我想下面的文字会慢慢让你明白的。
从Nokia的手机生产线说起一个“三层结构”的Web应用程序,就象是Nokia公司的手机生产线。
☐Web层就像是公司的经理,他负责洞察市场趋势,决策产品的生产。
并根据市场筹策下一步计划。
☐InterService就像是公司的管理员,他主要负责管理下层员工,传达上级布置的生产任务给员工,并将生产结果反馈给上级Web。
☐AccessTask就是公司里的工人,他们主要是负责手机产品的生产装配工作,并将生产结果反馈给上级InterService。
他们并不需要知道产品将销往何处,也不用关心产品销量。
只要能完成任务,就可以拿到报酬。
命令方向是自上而下的,而结果反馈方向则是自下而上的。
根据这个图例来简要的描述彬月论坛中的留言板显示功能,那么代码应该是:<!--//首先是ListLeaveWord.aspx这个文件//--><Asp:Repeater id=″leaveWordRepeater″Runat=″SERVER″><ItemTemplate><%# DataItem.Eval(Container.DataItem, ″Content″) %></ItemTemplate>这样便完成了对留言板的访问和显示,箭头所指的方向就是命令的方向。
虽然这符合“三层结构”开发模式的思想,但是这却存在着重大的漏洞,或者说是重大缺陷!为什么会这么说呢?因为从中间层返回的结果是不安全的!而造成中间层返回结果不安全的原因是从数据库层返回的结果并不确切!这会造成外观层过于脆弱,这并不是一个“强”三层结构。
还是用代码来说明。
假如,LeaveWordDBTask.cs文件中的List方法实现是这样的://位于数据库层的 LeaveWordDBTask.cs 文件namespace AccessTask{public class LeaveWordDBTask{public void List(DataSet ds){string cmdText=″SELECT * FROM [RegUser]″; //注意这里,访问的不是LeaveWord数据表OleDbConnection dbConn=new OleDbConnection(″...″);OleDbDataAdapter dbAdp=new OleDbDataAdapter(cmdText, dbConn);dbAdp.Fill(ds, ″LeaveWord″); //但是也填充了DataSet}}那么回逆到文件LeaveWordService.cs的List函数,再回逆到ListLeaveWord.aspx.cs文件的LeaveWordDataBind 函数。
把数据绑定到重复器上,而在显示的时候,会提示:找不到Content字段!<Asp:Repeater id=″leaveWordRepeater″Runat=″SERVER″><ItemTemplate><%# DataItem.Eval(Container.DataItem, ″Content″) %><-- 在这里会出现错误提示</ItemTemplate></Asp:Repeater>出现这样的结果并不奇怪,因为数据库层访问的是RegUser数据表,而RegUser数据表中并没有定义Content字段。
外观层因此变得很脆弱,这也使得页面设计师和数据库编程人员产生了不应有的交涉。
仅仅为了达到程序可运行目的,数据库编程人员就必须小心翼翼的写每句代码。
这就像是NoKia公司的经理发布生产命令后,得到的返回结果却是生产线上的员工生产装配了好几台电视?!这当然不是经理们想要的结果。
但为什么会有这样结果呢?因为经理们在发布生产命令时,忘记说明产品的规格和特征了。
经理一声令下:“生产!”——(new LeaveWordService()).List(DataSet ds)但是却没有对产品的规格特征作详细说明?例如手机的型号、外观等等。
这里的ds就相当于所要生产的产品集合,但却没有作细部说明…那么怎样才能避免这样荒唐的结果出现呢?经理在发布生产命令之前,应该规定产品的规格特征!那么原来的示意图应该也发生一些变化:相应的代码也要变化:这样,即便是将LeaveWordTask.List方法修改成访问RegUser数据表的代码,也依然不会影响到外观层。
再执行期间系统会抛出异常,而这个异常信息肯定是再数据库层。
再有,因为位于外观层的重复器控件绑定的是数据库层返回结果的目的。