浅谈“三层结构”原理与用意
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
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
对象
|
#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
<?
xml
version
="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 文件也要作以修改:
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
页面加载函数
1
|
2
|
3
|
7
|
8
|
#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
<?
xml
version
="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
>
|
4
|
5
|
6
|
7
|
4
|
5
|
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.youkuaiyun.com/t/20060120/10/4526856.html