小菜编程成长记(十三 有了门面,程序员的程序会更加体面!)
(续上篇)
大鸟说道:“实际上没有学过设计模式去理解三层架构会有失偏颇的,毕竟分层是更高一级别的模式,所谓的架构模式。不过在程序中,有意识的遵循设计原则,却也可以有效的做出好的设计。”
“不要告诉我,刚才讲的‘迪米特法则’就会在分层中用得上?”小菜说。
“当然用得上,否则讲它干吗,你当我是在安慰你而临时编个法则来骗骗你呀?来,再来看看你上次写的代码。”
2 {
3 //读配置文件
4 ds = new DataSet();
5 ds.ReadXml(Application.StartupPath + "\\CashAcceptType.xml");
6 //将读取到的记录绑定到下拉列表框中
7 foreach (DataRowView dr in ds.Tables[0].DefaultView)
8 {
9 cbxType.Items.Add(dr["name"].ToString());
10 }
11 cbxType.SelectedIndex = 0;
12 }
13
“这是Form_Load的代码,里面有没有什么与界面无关的东西?”大鸟问道。
“第4、5行是读配置文件的代码,它应该属于DAL层。对吧?”
“很好,再看下面的这段,里面又有哪些呢?”
2 {
3 CashContext cc = new CashContext();
4 //根据用户的选项,查询用户选择项的相关行
5 DataRow dr = ((DataRow[])ds.Tables[0].Select("name='" + cbxType.SelectedItem.ToString()+"'"))[0];
6 //声明一个参数的对象数组
7 object[] args =null;
8 //若有参数,则将其分割成字符串数组,用于实例化时所用的参数
9 if (dr["para"].ToString() != "")
10 args = dr["para"].ToString().Split(',');
11 //通过反射实例化出相应的算法对象
12 cc.setBehavior((CashSuper)Assembly.Load("商场管理软件").CreateInstance("商场管理软件." + dr["class"].ToString(),
13 false, BindingFlags.Default, null, args, null, null));
14
15 double totalPrices = 0d;
16 totalPrices = cc.GetResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
17 total = total + totalPrices;
18 lbxList.Items.Add("单价:" + txtPrice.Text + " 数量:" + txtNum.Text + " "+cbxType.SelectedItem+ " 合计:" + totalPrices.ToString());
19 lblResult.Text = total.ToString();
20 }
“这里3-13行,是为确定哪种算法而创建CashContext对象,其中用到了反射技术,为计算做准备。第16行是真正的计算打折价或返利,17-19是界面显示的部分。所以应该把3-16行都搬到BLL层去。不过,我还有些疑问,这样做会让配置文件的数据要先从DAL转到BLL,再转到表示层,多麻烦呀,什么不直接表示层读DAL,它想要数据就去读DAL,它想算结果就去请求BLL处理?”
“那是说明你没有真的了解什么叫迪米特法则,象你那样说,不就等于,你小菜又要认识小张,又要认识小李了,这不就耦合过度吗?本来你只需要认识一个人就可以了,这样依赖才会小呀!”
“可是我就得在BLL里写一个专门返回从DAL里得到数据的方法,这个方法不属于现在的任何类,我就还得再写一个类来做这种传声筒的角色。而且由于界面还要涉及到其它的类,如CashContext,感觉UI和BLL耦合还是很高。”
“说得没错,你的确是讲到点子上了,由于表示层UI需要与BLL有两个类进行交互,这是很麻烦,不过前辈们就想了了一个较好的办法,另一个设计模式,‘门面模式’(Facade)或叫外观模式”
(以下源自吕震宇 博客)
门面模式要求一个子系统的外部与其内部的通信必须通过一个统一的门面(Facade)对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
门面模式的结构
门面模式是对象的结构模式。门面模式没有一个一般化的类图描述,下图演示了一个门面模式的示意性对象图:
在这个对象图中,出现了两个角色:
门面(Facade)角色:客户端可以调用这个角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去。
子系统(subsystem)角色:可以同时有一个或者多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。每一个子系统都可以被客户端直接调用,或者被门面角色调用。子系统并不知道门面的存在,对于子系统而言,门面仅仅是另外一个客户端而已。
“哦,你这样一讲,我就明白了。”小菜说,“上篇所讲的IT部,其实可以由部门主管就是门面,我们只需要找到部门主管,就可以通过他安排相关的人来提供服务,我们不需要了解IT部的具体情况了。”
“其实现实中这样的例子很多。比如以前上海市没有新闻发言人,当要到春运时,所有的记者都跑到交通部去了解信息,当有非典或禽流感时,所有的记者又跑到卫生部去打听情况,突然这时候楼市大跌,记者们又得马不停蹄前往建设部收集新闻。辛苦呀,有什么办法呢,吃这口饭的。但其实辛苦地又何止只是记者。各个政府部门都需要专人来应付这些记者,不能多说话,不能说错话,但也不能不说话。也辛苦呀,谁叫他们是政府呢。”大鸟仿佛自己感同身受似的描述着,“于是,新闻发言人横空出世,一位知识女性焦扬,代表上海市政府发言,从此,老记们不需要头顶骄阳奔跑于各大政府部门之间,只需要天天等在新闻发言厅门口守着就可以写出准确及时的新闻。而政府部门也不用专人来应付老记们的围追堵截,有更多的时间为人民做实事办好事。这里就只辛苦一个人。”
“那一定是新闻发言人自己了,因为她需要先与政府部门沟通好,要说些什么、如何说、如何回答***钻问题。然后要站在镁光灯下承受压力接受记者的访问。不过,干这一行就是需要辛苦的,这是政府的门面呀。”小菜感慨到。
“好了,去改写吧,你一定会感受到分层后代码的漂亮。”大鸟鼓励道。
过一小时后,小菜给出商场收银程序的第六份作业。。
DAL层代码(目前是读配置文件,以后可以很容易的修改为访问数据库)
using System.Collections.Generic;
using System.Text;
using System.Data;
namespace 商场管理软件.DAL
{
public class CashAcceptType
{
public DataSet GetCashAcceptType()
{
//读配置文件到DataSet
DataSet ds = new DataSet();
ds.ReadXml("CashAcceptType.xml");
return ds;
}
}
}
BLL层主要代码(Facade类代码)
namespace 商场管理软件.BLL
{
public class CashFacade
{
const string ASSEMBLY_NAME = "商场管理软件.BLL";
//得到现金收取类型列表,返回字符串数组
public string[] GetCashAcceptTypeList()
{
CashAcceptType cat = new CashAcceptType();
DataSet ds = cat.GetCashAcceptType();
int rowCount = ds.Tables[0].DefaultView.Count;
string[] arrarResult = new string[rowCount];
for (int i = 0; i < rowCount; i++)
{
arrarResult[i] = (string)ds.Tables[0].DefaultView[i]["name"];
}
return arrarResult;
}
/// <summary>
/// 用于根据商品活动的不同和原价格,计算此商品的实际收费
/// </summary>
/// <param name="selectValue">下拉列表选择的折价类型</param>
/// <param name="startTotal">原价</param>
/// <returns>实际价格</returns>
public double GetFactTotal(string selectValue, double startTotal)
{
CashAcceptType cat = new CashAcceptType();
DataSet ds = cat.GetCashAcceptType();
CashContext cc = new CashContext();
DataRow dr = ((DataRow[])ds.Tables[0].Select("name='" + selectValue + "'"))[0];
object[] args = null;
if (dr["para"].ToString() != "")
args = dr["para"].ToString().Split(',');
cc.setBehavior((CashSuper)Assembly.Load(ASSEMBLY_NAME).CreateInstance(ASSEMBLY_NAME + "."
+ dr["class"].ToString(), false, BindingFlags.Default, null, args, null, null));
return cc.GetResult(startTotal);
}
}
}
UI层代码(可以很容易的转换为Web页面)
double total = 0.0d;//用于总计
CashFacade cf = new CashFacade();
private void Form1_Load(object sender, EventArgs e)
{
//读数据绑定下拉列表
cbxType.DataSource=cf.GetCashAcceptTypeList();
cbxType.SelectedIndex = 0;
}
private void btnOk_Click(object sender, EventArgs e)
{
double totalPrices = 0d;
//传进下拉选择值和原价,计算实际收费结果
totalPrices = cf.GetFactTotal(cbxType.SelectedItem.ToString(), Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
total = total + totalPrices;
lbxList.Items.Add("单价:" + txtPrice.Text + " 数量:" + txtNum.Text + " "+cbxType.SelectedItem+ " 合计:" + totalPrices.ToString());
lblResult.Text = total.ToString();
}
项目文件结构图
“大鸟,来看看这下怎么样,还有没有可修改的地方?”小菜问道。
“小菜开始谦虚了吗!以前不是一直信誓旦旦,现在怎么,没信心了?”
“越学越觉得自己知道的少,感觉代码重构没有最好,只有更好呀。”小菜诚心的答道。
“写得很不错。BLL层的CashFacade类其实就是新闻发言人,程序的门面;而应用程序或Web其实就类似CCTV和SMG,都是新闻单位,他们不应该也不需要关心门面后面的实现是如何的。,现在用了门面模式以后,耦合比以前要少很多了,更改会更加方便,扩展也很容易了。你要是再回过头来看看最初的代码和现在的代码,你会体会更深刻,更加明白重构的魅力。”
大鸟接着说:“之前的代码,下拉控件的绑定是硬编码,所以只要改动需求就得改代码,现在是读配置文件,大大增加灵活性;之前的代码是根据用户选择,分支判断执行相应的算法,现在整个算法类全部搬走,做到了业务与界面的分离;之前的代码由于全写在form里,所以要更换成Web方式,即C/S改为B/S非常困难,要全部重新写(注意真实的软件系统不会这么简单,所以简单复制不能解决问题),现在的代码由于把业务运算分离,所以界面的更改不会影响业务的编写。还有就是现在的代码由于DAL与BLL分离,配置文件可以很容易的更换为数据库读取,且不需要影响表示层与业务逻辑层的代码。总的来讲,若是程序不会变化,原有的设计就没什么问题,运行结果正确足够了,但若是程序可能会时常随业务而变化,新的设计就大大提高了应变性,这其实就是应用设计模式的目的所在。”
“我现在越来越有信心学好它,设计模式真的很有意思,学它不学它,写出来的代码大不一样。老大,跟你混,看来没有错。”
“嗨,小菜,我不做老大已经很久了!”大鸟仰身长叹,扬长而去。
(待续)
本文源代码。其中分四个项目,DAL、BLL、WebUI和WinUI,可设置WebUI和WinUI为启动项目,注意由于只是学习源代码,配置路径没有做处理(实际应用需要config文件),WebUI配置文件CashAcceptType.xml在“\商场管理软件06分层\”根目录下,而WinUI的配置文件CashAcceptType.xml在“\商场管理软件06分层\商场管理软件\bin\Debug\”目录下。