lcwdzl

博客园 首页 新随笔 联系 订阅 管理
 

“三层结构”是什么?

  “三层结构”一词中的“三层”是指:“表现层”、“中间业务层”、“数据访问层”。其中:

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

#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.csPostLWord.aspx.cs这两个文件已经找不到和数据库相关的代码了。只看到一些和LWordTask类有关系的代码,这就符合了“设计模式”中的一种重要原则:“迪米特法则”。“迪米特法则”主要是说:让一个“类”与尽量少的其它的类发生关系。TraceLWord1中,ListLWord.aspx.cs这个类和OleDbConnectionOleDbDataAdapter都发生了关系,所以它破坏了“迪米特法则”。利用一个“中间人”是“迪米特法则”解决问题的办法,这也是“门面模式”必须遵循的原则。下面就引出这个LWordTask门面类的示意图:

ListLWord.aspx.csPostLWord.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)...

}

}


用户在访问TraceLWord3ListLWord.aspx页面时序图:

当一个用户访问TraceLWord5ListLWord.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.0Duwamish7的研究,并不是本文的重点。现在的问题就是:既然“三层结构”已经被分派到各自的项目中,那么剩下来的项目是做什么的呢?例如PetShop3.0中的“Model”、“IDAL”、“DALFactory”这三个项目,再例如Duwamish7中的“Common”项目,还有就是在Bincess.CN彬月论坛中的“Classes”、“DbTask”、这两个项目。它们究竟是做什么用的呢?

posted on 2007-09-13 20:04  Lester  阅读(277)  评论(0)    收藏  举报