用户在访问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 }
通过数据访问层工厂来制造对象实例,而工厂类
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确要经过多次调用才能获取到数据。在子程序模块程序没有返回时,主程序模块只能处于等待状态。所以在执行速度上,留言板的版本越高,排名却越靠后。“三层结构”开发模式,不适用于对执行速度要求过于苛刻的系统,例如:在线订票,在线炒股等等……它比较擅长于商业规则容易变化的系统。
其实,无论哪一种开发模式或方法,都是有利有弊的。不会存在一种“万用法”可以解决任何问题。所以“三层结构”这个词眼也不会是个例外!是否采用这个模式进行系统开发,要作出比较、权衡之后才可以。切忌滥用——
最后要声明的是我不是这篇文章的作者,让我们对三层架构有这么多认识
更要感谢我们的作者 可是我也不知道是那位高人写的而且还这么详细 真是辛苦他了。
任务完成了!忘能帮助大家
祝君成功!