高度的“面向对象思想”的体现——封装
在我们去餐馆吃饭的这个过程中,像我这样在餐馆中的吃客,最关心的是什么呢?当然是:餐馆的饭菜是不是好吃,是不是很卫生?价格是不是公道?……而餐馆中的服务生会关心什么呢?应该是:要随时注意响应每位顾客的吩咐,要记住顾客在哪个桌位上?还要把顾客点的菜记在本子上……餐馆的大厨师傅会关心什么呢?应该是:一道菜肴的做法是什么?怎么提高烧菜的效率?研究新菜式……大厨师傅,烧好菜肴之后,只管把菜交给服务生就完事了。至于服务生把菜送到哪个桌位上去了?是哪个顾客吃了他做的菜,大厨师傅才不管咧——服务生只要记得把我点的菜肴端来,就成了。至于这菜是怎么烹饪的?顾客干麻要点这道菜?他才不管咧——而我,只要知道这菜味道不错,价格公道,干净卫生,其他的我才不管咧——
这里面不正是高度的体现了“面向对象思想”的“封装”原则吗?
无论大厨师傅在什么时候研究出新的菜式,都不会耽误我现在吃饭。就算服务生忘记我的桌位号是多少了,也不可能因此让大厨师傅忘记菜肴的做法?在我去餐馆吃饭的这个过程中,我、餐馆服务生、大厨师傅,是封装程度极高的三个个体。当其中的一个个体内部发生变化的时候,并不会波及到其他个体。这便是面向对象封装特性的一个益处!
土豆炖牛肉盖饭与实体规范
在我工作过的第一家公司楼下,有一家成都风味的小餐馆,每天中午我都和几个同事一起去那家小餐馆吃饭。公司附近只有这么一家餐馆,不过那里的饭菜还算不错。我最喜欢那里的“土豆炖牛肉盖饭”,也很喜欢那里的“鸡蛋汤”,那种美味至今难忘……所谓“盖饭”,又称是“盖浇饭”,就是把烹饪好的菜肴直接遮盖在铺在盘子里的米饭上。例如“土豆炖牛肉盖饭”,就是把一锅热气腾腾的“土豆炖牛肉”遮盖在米饭上——
当我和同事再次来到这家餐馆吃饭,让我们想象以下这样的情形:
情形一:
我对服务生道:给我一份好吃的!
服务生道:什么好吃的?
我答道:一份好吃的——
三番几次……
我对服务生大怒道:好吃的,好吃的,你难道不明白吗?!——
这样的情况是没有可能发生的!因为我没有明确地说出来我到底要吃什么?所以服务生也没办法为我服务……
问题后果:我可能被送往附近医院的精神科……
情形二:
我对服务生道:给我一份土豆炖牛肉盖饭!
服务生对大厨师傅道:做一份宫爆鸡丁——
这样的情况是没有可能发生的!因为我非常明确地说出来我要吃土豆炖牛肉盖饭!但是服务生却给我端上了一盘宫爆鸡丁?!
问题后果:我会投诉这个服务生的……
情形三:
我对服务生道:给我一份土豆炖牛肉盖饭!
服务生对大厨师傅道:做一份土豆炖牛肉盖饭——
大厨师傅道:宫爆鸡丁做好了……
这样的情况是没有可能发生的!因为我非常明确地说出来我要吃土豆炖牛肉盖饭!服务生也很明确地要求大厨师傅做一份土豆炖牛肉盖饭。但是厨师却烹制了一盘宫爆鸡丁?!
问题后果:我会投诉这家餐馆的……
情形四:
我对服务生道:给一份土豆炖牛肉盖饭!
服务生对大厨师傅道:做一份土豆炖牛肉盖饭——
大厨师傅道:土豆炖牛肉盖饭做好了……
服务生把盖饭端上来,放到我所在的桌位。我看着香喷喷的土豆炖牛肉盖饭,举勺下口正要吃的时候,却突然发现这盘土豆炖牛肉盖饭变成了石头?!
这样的情况更是没有可能发生的!必定,现实生活不是《西游记》。必定,这篇文章是学术文章而不是《哈里波特》……
问题后果:……
如果上面这些荒唐的事情都成了现实,那么我肯定永远都不敢再来这家餐馆吃饭了。这些让我感到极大的不安。而在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数据库平台上……