读《戏 说 面 向 对 象 程 序》后感

面试受挫——代码无错就是好?

上面写着,“请用 C++JavaC# VB.NET 任意一种面向对象语言实

现一个计算器控制台程序,要求输入两个数和运算符号,得到结果。”

class Program

{

     static void Main(string[] args)

{

Console.Write("请输入数字 A");

string A = Console.ReadLine();

Console.Write("请选择运算符号(+-*/)");

string B = Console.ReadLine();

Console.Write("请输入数字 B");

string C = Console.ReadLine();

string D = "";

if (B == "+")

      D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));

if (B == "-")

      D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));

if (B == "*")

      D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));

if (O == "/")

      D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));

Console.WriteLine("结果是:" + D);

}

}

代码有什么问题呢?


 代码规范、重构

且先不说出题人的意思,单就你现在的代码,就有很多不足的地方需要改进。比如变量命名,你的命名

就是 ABCD,变量不带有任何具体含义,这是非常不规范的;判断分支,你这样的写法,意味着每个条件都要做判断,

等于计算机做了三次无用功;数据输入有效性判断等,如果用户输入的是字符符号而不是数字怎么办?如果除数时,

客户输入了 0怎么办?这些都是可以改进的地方。”

   在更改之后

class Program

{

     static void Main(string[] args)

     {

     try

     {

          Console.Write("请输入数字 A");

          string strNumberA = Console.ReadLine();

Console.Write("请选择运算符号(+-*/)");

string strOperate = Console.ReadLine();

Console.Write("请输入数字 B");

string strNumberB = Console.ReadLine();

string strResult = "";

switch (strOperate)

{

case "+":

     strResult = Convert.ToString(Convert.ToDouble(strNumberA) + Convert.ToDouble(strNumberB));

     break;

case "-":

     strResult = Convert.ToString(Convert.ToDouble(strNumberA) - Convert.ToDouble(strNumberB));

     break;

case "*":

     strResult = Convert.ToString(Convert.ToDouble(strNumberA) * Convert.ToDouble(strNumberB));

     break;

case "/":

     if (strNumberB != "0")

     strResult= Convert.ToString(Convert.ToDouble(strNumberA) / Convert.ToDouble(strNumberB));

else

strResult = "除数不能为 0";

break;

}

Console.WriteLine("结果是:" + strResult);

Console.ReadLine();

}

catch (Exception ex)

{

     Console.WriteLine("您的输入有错:" + ex.Message);


}

}

}

至在目前代码来说,实现计算器是没有问题了,但这样写出的代码是否

合出题人的意思呢?”

我们是否可以考虑一下用面向对象的思想方法去做下一呢

 复制 VS 复用

任意一种面向对象语言实现,那意思就是要用面向对象的编程方法去实现,

“所有编程初学者都会有这样的问题,就是碰到问题就直觉的用计算机能够理解的逻辑来描述和表达待解决

的问题及具体的求解过程。这其实是用计算机的方式去思考,比如计算器这个程序,先要求输入两个数和运算符号,

然后根据运算符号判断选择如何运算,得到结果,这本身没有错,但这样的思维却使得我们的程序只为满足实现当前

的需求,程序不容易维护,不容易扩展,更不容易复用。从而达不到高质量代码的要求。”

有人说初级程序员的工作就是 Ctrl+CCtrl+V,这其实是非常不好的编码习惯,

因为当你的代码中重复的代码多到一定程度,维护的时候,可能就是一场灾难。越大的系统,这种方式带来的问题越

严重,编程有一原则,就是用尽可能的办法去避免重复。想想看,你写的这段代码,有哪些是和控制台无关的,而只

是和计算器有关的?”


 业务的封装

分一个类出来? 哦,对的,让计算和显示分开。

准确的说,就是让业务逻辑与界面逻辑分开,让它们之间的耦合度下降。只有分离开,才容易达到容易维

护或扩展。”

class Program

{

static void Main(string[] args)

{

     try

     {

     Console.Write("请输入数字 A");

     string strNumberA = Console.ReadLine();

Console.Write("请选择运算符号(+-*/)");

string strOperate = Console.ReadLine();

Console.Write("请输入数字 B");

string strNumberB = Console.ReadLine();

string strResult = "";

strResult=

   Convert.ToString(Operation.GetResult(Convert.ToDouble(strNumberA),Convert.ToDouble(strNumberB),strOperate

   ));

Console.WriteLine("结果是:" + strResult);

Console.ReadLine();

}

catch (Exception ex)

{

Console.WriteLine("您的输入有错:" + ex.Message);

}

}

}

public class Operation

{

public static double GetResult(double numberA,double numberB,string operate)

{

    double result = 0d;

    switch (operate)

    {

case "+":

     result = numberA + numberB;

     break;

case "-":


     result = numberA - numberB;

     break;

case "*":

     result = numberA * numberB;

     break;

case "/":

result = numberA / numberB;

break;

}

return result;

}

}


不单是 Windows程序,Web版程序需要运算可以用它,PDA,手机等需要移动系统的软件需要运算也可

以用它呀。

这里我们运用了面向对象的三大特性之一的封装,

体会简单工厂模式的美妙


现在公司要求你为公司的薪资管理系统做维护,原来只有技术人员

(月薪),市场销售人员(底薪+提成),经理(年薪+股份)三种运算算法,现在要增加兼职工作人员的(时薪)算法,

但按照你昨天的程序写法,公司就必须要把包含有的原三种算法的运算类给你,让你修改,你如果心中小算盘一打,

‘TMD,公司给我的工资这么低,我真是郁闷,这会有机会了’,于是你除了增加了兼职算法以外,在技术人员(月薪)

算法中写了一句


if (员工是小菜)

{

    salary = salary * 1.1;

}

那就意味着,你的月薪每月都会增加 10%(小心被抓去坐牢),本来是让你加一个功能,却使得原有的运行良好的功能

代码产生了变化,这个风险太大了。你明白了吗?”

/** <summary>

/// 运算类

/// </summary>

class Operation

{

private double _numberA = 0;

private double _numberB = 0;

/** <summary>

/// 数字A

/// </summary>

public double NumberA

{

      get{ return _numberA; }

      set{ _numberA = value;}

}

/** <summary>

/// 数字B

/// </summary>

public double NumberB

{

      get{ return _numberB; }

      set{ _numberB = value; }

}

/** <summary>

/// 得到运算结果

/// </summary>

/// <returns></returns>

public virtual double GetResult()

{

      double result = 0;

      return result;

}

}


/** <summary>

/// 加法类

/// </summary>

class OperationAdd : Operation

{

public override double GetResult()

{

double result = 0;

result = NumberA + NumberB;

return result;

}

}

/** <summary>

/// 减法类

/// </summary>

class OperationSub : Operation

{

    public override double GetResult()

{

     double result = 0;

     result = NumberA - NumberB;

     return result;

}

}

/** <summary>

/// 乘法类

/// </summary>

class OperationMul : Operation

{

public override double GetResult()

{

      double result = 0;

      result = NumberA * NumberB;

      return result;

}

}

/** <summary>

/// 除法类

/// </summary>

class OperationDiv : Operation

{

public override double GetResult()

{

double result = 0;

if (NumberB==0)

throw new Exception("除数不能为 0");

result = NumberA / NumberB;


return result;

}

}

首先是一个运算类,它有两个 Number属性,主要用于计算

器的前后数,然后有一个虚方法 GetResult(),用于得到结果,然后我把加减乘除都写成了运算类的子类,继承它后,

重写了 GetResult()方法,这样如果要修改任何一个算法,都不需要提供其它算法的代码了。但问题来了,我如何让计

算器知道我是希望用哪一个算法呢?”

也就是说,到底要实例化谁,将来会不会增加实例化的对象(比如增加开根运算),

这是很容易变化的地方,应该考虑用一个单独的类来做这个创造实例的过程,这就是工厂,来,我们看看这个类如何

写。”

/** <summary>

/// 运算类工厂

/// </summary>

class OperationFactory

{

public static Operation createOperate(string operate)

{

    Operation oper = null;

    switch (operate)

    {

    case "+":

          {

          oper = new OperationAdd();

          break;

          }

case "-":

     {

     oper = new OperationSub();

     break;

     }

case "*":

     {

     oper = new OperationMul();

     break;

     }

case "/":

     {

     oper = new OperationDiv();

     break;

     }

 }

return oper;

}

}

看到吧,这样子,你只需要输入运算符号,工厂就实例化出合适的对象,通过多态,返回父类的方

式实现了计算器的结果。”


Operation oper;

oper = OperationFactory.createOperate("+");

oper.NumberA = 1;

oper.NumberB = 2;

double result = oper.GetResult();

界面的实现就是这样的代码,不管你是控制台程序,Windows 程序,Web 程序,PDA 或手机程序,

都可以用这段代码来实现计算器的功能,当有一天我们需要更改加法运算,我们只需要改哪里?”

“改 OperationAdd就可以了。”

(下为当时面试题时所写代码,见《第一章》)

class Program

{

     static void Main(string[] args)

     {

     Console.Write("请输入数字 A");

     string A = Console.ReadLine();

Console.Write("请选择运算符号(+-*/)");

string B = Console.ReadLine();

Console.Write("请输入数字 B");

string C = Console.ReadLine();

string D = "";

if (B == "+")

      D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));

if (B == "-")

      D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));

if (B == "*")

      D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));

if (O == "/")

      D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));

Console.WriteLine("结果是:" + D);

}

编程是一门技术,更加是一门艺术,不能只满足于写完代码运行结果正确就完事,时常

考虑如何让代码更加简炼,更加容易维护,容易扩展和复用,只有这样才可以是真的提高。写出优雅的代码真的是一

种很爽的事情。不过学无止境,其实这才是理解面向对象的开始呢。给你出个作业,做一个商场收银软件,营业员根

据客户购买商品单价和数量,向客户收费。”


 工厂不好用了?

一个商场收银软件,营业员根据客户购买商品单价和数量,向客户收费。这个很

代码样例(可使用):

商场收银系统 v1.0关键代码如下:

//声明一个 double变量total来计算总计

  double total = 0.0d;

  private void btnOk_Click(object sender, EventArgs e)

  {

       //声明一个 double变量totalPrices来计算每个商品的单价(txtPrice)*数量(txtNum)后的合计

       double totalPrices=Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text);

//将每个商品合计计入总计

total = total + totalPrices;

//在列表框中显示信息

lbxList.Items.Add("单价:"+txtPrice.Text+"数量:"+txtNum.Text+"合计:"+totalPrices.ToString());

// lblResult标签上显示总计数

lblResult.Text = total.ToString();

}

要求商场对商品搞活动,所有的商品打8

商场收银系统 v1.1关键代码如下:

double total = 0.0d;

    private void btnOk_Click(object sender, EventArgs e)

    {

         double totalPrices=0d;

         //cbxType 是一个下拉选择框,分别有“正常收费” “打8折” “打7折”和“打5折”、、

         switch(cbxType.SelectedIndex)

         {

         case 0:

               totalPrices = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text);

               break;

case 1:

     totalPrices = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text) * 0.8;

     break;

case 2:

     totalPrices = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text) * 0.7;

     break;

case 3:

     totalPrices = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text) * 0.5;

     break;

          }

          total = total + totalPrices;

          lbxList.Items.Add("单价:" + txtPrice.Text + "数量:"+ txtNum.Text + " "+cbxType.SelectedItem+ "合计:

" + totalPrices.ToString());

          lblResult.Text = total.ToString();

     }

    事先把商场可能的打折都做成下拉选择框的项,要变化的可能性就小多了   “这比刚才灵活性上是好多了,不过重复代码很多,像Convert.ToDouble(),你这里就写了8遍,而且4个分

支要执行的语句除了打折多少以外几乎没什么不同,应该考虑重构一下。不过还不是最主要的,现在我的需求又来了,

商场的活动加大,需要有满 300100的促销算法,你说怎么办?”

“面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对

象的抽象集合才是类 。打一折和打九折只是形式的不同,抽象分析出来,所有的打折算法都是一样的,所以打折算法

应该是一个类。好了,空话已说了太多,写出来再是真的懂。”


商场收银系统 v1.3关键代码如下

//现金收取父类

abstract class CashSuper

{

//抽象方法:收取现金,参数为原价,返回为当前价

public abstract double acceptCash(double money);

}

//正常收费,继承 CashSuper

class CashNormal : CashSuper

{

public override double acceptCash(double money)

{

     return money;

}

}

//打折收费,继承 CashSuper

class CashRebate : CashSuper

{

private double moneyRebate = 1d;

//初始化时,必需要输入折扣率,如八折,就是 0.8

public CashRebate(string moneyRebate)

{

     this.moneyRebate = double.Parse(moneyRebate);

}

public override double acceptCash(double money)

{


return money * moneyRebate;

}

}

//返利收费,继承 CashSuper

class CashReturn : CashSuper

{

private double moneyCondition = 0.0d;

private double moneyReturn = 0.0d;

//初始化时必须要输入返利条件和返利值,比如满 300100,则 moneyCondition300moneyReturn100

public CashReturn(string moneyCondition, string moneyReturn)

{

    this.moneyCondition = double.Parse(moneyCondition);

    this.moneyReturn = double.Parse(moneyReturn);

}

public override double acceptCash(double money)

{

double result = money;

//若大于返利条件,则需要减去返利值

if (money >= moneyCondition)

result = money - Math.Floor(money / moneyCondition) * moneyReturn;

return result;

}

}

//现金收取工厂

class CashFactory

{

//根据条件返回相应的对象

public static CashSuper createCashAccept(string type)

{

     CashSuper cs = null;

     switch (type)

     {

     case "正常收费":

          cs = new CashNormal();

     break;

case " 300100":

     CashReturn cr1 = new CashReturn("300", "100");

     cs = cr1;

     break;

case " 8折":

     CashRebate cr2 = new CashRebate("0.8");

     cs = cr2;

     break;

}

return cs;

}

}


//客户端窗体程序(主要部分)

CashSuper csuper;//声明一个父类对象

double total = 0.0d;

private void btnOk_Click(object sender, EventArgs e)

{

//利用简单工厂模式根据下拉选择框,生成相应的对象

     csuper = CashFactory.createCashAccept(cbxType.SelectedItem.ToString());

     double totalPrices=0d;

     //通过多态,可以得到收取费用的结果

     totalPrices = csuper.acceptCash(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();

     }

代码样例(可使用)

“我要是需要打 5折和满500200的促销活动,如何办?”

“只要在现金工厂当中加两个条件,在界面的下拉选项框里加两项,就 OK 了。”

“现金工厂?!你当是生产钞票呀。是收费对象生成工厂才准确。说得不错,如果我现在需要增加一种商场促

销手段,满 100积分10点,以后积分到一定时候可以领取奖品如何做?”

“有了工厂,何难?加一个积分算法,构造方法有两个参数:条件和返点,让它继承 CashSuper,再到现金工厂,

哦,不对,,是收—费—对—象—生—成—工—厂里加满 100积分10点的分支条件,再到界面稍加改动,就行了。”

“嗯,不错,那我问你,如果商场现在需要拆迁,没办法,只能跳楼价销售,商场的所有商品都需要打8折,

打折后的价钱再每种商品满 30050,最后计总价的时候,商场还满 1000200,你说如何办?


“简单工厂模式虽然也能解决这个问题,但的确不是最好的办法,另外由于商场是可能经常性的更改

打折额度和返利额度,每次更改都需要改写代码重新编译部署真的是很糟糕的处理方式,面对算法的时常变动,应该

有更好的办法。好好去研究一下设计模式吧,

用“策略模式”是一种好策略

策略模式(Strategy)。“『策略模式』

定义了算法家族,分别封装起来,让它们之间可以互相替换, 此模式让算法的变化, 不会影响到使用算法的客户。

看来商场收银系统应该考虑用策略模式?”

“商场收银时如何促销,用打折还是返利,其实都是一些算法,用工厂来生成

算法对象,感觉是不是很怪?而最重要的是这些算法是随时都可能互相替换的,这就是变化点,而封装变化点是我们

面向对象的一种很重要的思维方式。”

策略模式的结构 (源自吕震宇 博客)

这个模式涉及到三个角色:

环境(Context)角色:持有一个 Strategy 类的引用。

抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的

接口。

具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。

CashSuper就是抽象策略,而正常收费CashNormal、打折收费 CashRebate

和 返 利 收 费 CashReturn就 是 三 个 具 体 策 略 , 也 就 是 策 略 模 式 中 说 的 具 体 算 法 ”



CashContext 类代码如下:

    //收费策略 Context

    class CashContext

    {

    //声明一个现金收费父类对象

    private CashSuper cs;

//设置策略行为,参数为具体的现金收费子类(正常,打折或返利)

public void setBehavior(CashSuper csuper)

{

    this.cs = csuper;

}

//得到现金促销计算结果(利用了多态机制,不同的策略行为导致不同的结果)

public double GetResult(double money)

{

return cs.acceptCash(money);

}

}


客户端主要代码如下:

   double total = 0.0d;//用于总计

private void btnOk_Click(object sender, EventArgs e)

{

     CashContext cc = new CashContext();

     switch (cbxType.SelectedItem.ToString())

{

case "正常收费":

     cc.setBehavior(new CashNormal());

     break;

case " 300100":

     cc.setBehavior(new CashReturn("300","100"));

     break;

case " 8折":

     cc.setBehavior(new CashRebate("0.8"));

     break;

}

          double totalPrices = 0d;

          totalPrices = cc.GetResult(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();

     }

实现的界面同之前一样

策略模式是实现了,但有些疑问,用了策略模式,则把分支判断又放回到客户端来了,这等于要

改变需求算法时,还是要去更改客户端的程序呀?”


最初的策略模式是有缺点的,客户端必须知道所有的策略类,并自行决定使用

哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用

于客户端知道所有的算法或行为的情况。”

“那还不如工厂模式好用,至少要增加促销或改进打折额度时,不用去大改界面,而现在,界面程序要承担的责

任还是太大。没有体现你说的封装变化点的作用呀。”。

 “就目前而言,的确是这样,这样的程序确实还是不够完善,要改的地方还很多。”

毒时间长了会有变种,杀毒软件本身也会随着病毒的变化而升级改良,如果我们对策略模式做一些改进,引入一些新

的技术处理方式,就可以避免现在的这种耦合了。小菜,又有新的东西要学了,好好加油呀!”

学习一定是一个自己感悟的过程,而程序员的感悟就是自己写程序做项目,通过实践再学习,最终升华为

牛人。”

反射——程序员的快乐!

“到底如何去改良策略模式呢?”

“仔细观察过没有,代码,不管是用工厂模式写的,还是用策略模式写的,那个分支的switch依然去不掉。

因为程序里有下拉选择,用户是有选择的,那么程序就必须要根据用户的选择来决定实例化哪一个子类对象。

无论是在客户端窗体类编程还是到工厂类里编程,这个 switch总是少不掉的。问题主要出在这里。 小菜十分肯定的说。”

 所以我们要考虑的就是可不可以不在程序里写明‘如果是打折就去实例化CashRebate类,如

果是返利就去实例化 CashReturn类’这样的语句,而是在当用户做了下拉选择后,再根据用户的选择去某个地方找应

该要实例化的类是哪一个。这样,我们的 switch就可以对它说再见了。”要说的就是一种编程方式:依赖注入(DependencyInjection,从字面上不太好理解,我们也不去管它。关

键在于如何去用这种方法来解决我们的 switch问题。本来依赖注入是需要专门的IoC容器提供,比如spring.net,显然

当前这个程序不需要这么麻烦,你只需要再了解一个简单的.net 技术‘反射’就可以了。”


“看下面的两个样例:

1//实例化方法一

2//原来我们把一个类实例化是这样的

3Animal animal=new Cat(); //声明一个动物对象,名称叫 animal,然后将 animal实例化成猫类的对象

4

5//实例化方法二

6//我们还可以用反射的办法得到这个实例

7using System.Reflection;//先引用 System.Reflection

8//假设当前程序集是 AnimalSystem,名称空间也是 AnimalSystem

9Animal animal = (Animal)Assembly.Load("AnimalSystem").CreateInstance("AnimalSystem.Cat");

 其中关键是

Assembly.Load("程序集名称").CreateInstance("名称空间.类名称")

那也就是说,我们可以在实例化的时候,再给计算机一个类的名称字符串,来让计算机知道应该实例化哪一个类。”


       “  意 思 是 , 之 前 写 的 ‘cc.setBehavior(new CashNormal());’ 可 以 改 写 为

‘cc.setBehavior((CashSuper)Assembly.Load("商场管理软件").CreateInstance("商场管理软件.CashNormal")’,

   “分析一下,原来new CashNormal()是什么?是否是写死在程序里的代码,可以灵活更换吗?

   “不可以,那还换什么,写什么就是什么了呗。”

   “那说,在反射中的CreateInstance("商场管理软件.CashNormal"),可以灵活更换‘CashNormal’吗?

字符串,可以用变量来处理,也就可以根据需要更换。

“读数据库当然最好了,其实用不着这么麻烦,不是有 XML这个东东吗,写个配置文件不就解决了?”

让它去读 XML的配置文件,来生成这个下拉列表框,然后再根据用户的选择,通过反

射实时的实例化出相应的算法对象,最终利用策略模式计算最终的结果

 “OK,还有一个小细节,你的CashRebateCashReturn在构造函数中都是有参数的,这需要用到CreateInstance()

客户端主要代码:

using System.Reflection;

   DataSet ds;//用于存放配置文件信息

double total = 0.0d;//用于总计

private void Form1_Load(object sender, EventArgs e)

{

     //读配置文件

     ds = new DataSet();

ds.ReadXml(Application.StartupPath + "\\CashAcceptType.xml");

//将读取到的记录绑定到下拉列表框中

foreach (DataRowView dr in ds.Tables[0].DefaultView)

{

cbxType.Items.Add(dr["name"].ToString());

}

cbxType.SelectedIndex = 0;

}

private void btnOk_Click(object sender, EventArgs e)

{

         CashContext cc = new CashContext();

         //根据用户的选项,查询用户选择项的相关行

         DataRow dr = ((DataRow[])ds.Tables[0].Select("name='" + cbxType.SelectedItem.ToString()+"'"))[0];

         //声明一个参数的对象数组

         object[] args =null;

         //若有参数,则将其分割成字符串数组,用于实例化时所用的参数

         if (dr["para"].ToString() != "")

         args = dr["para"].ToString().Split(',');

         //通过反射实例化出相应的算法对象

         cc.setBehavior((CashSuper)Assembly.Load("商 场 管 理 软 件").CreateInstance("商 场 管 理 软 件." +

dr["class"].ToString(), false, BindingFlags.Default, null, args, null, null));

          double totalPrices = 0d;

          totalPrices = cc.GetResult(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();

     }

<?xml version="1.0" encoding="utf-8" ?>
<CashAcceptType>
<type>
<name>正常收费</name>
<class>CashNormal</class>
<para></para>
</type>
<type>
<name>满 300 返 100</name>
<class>CashReturn</class>
<para>300,100</para>
</type>
<type>
<name>满 200 返 50</name>
<class>CashReturn</class>
<para>200,50</para>
</type>
<type>
<name>打 8 折</name>
<class>CashRebate</class>
<para>0.8</para>
</type>
<type>
<name>打 7 折</name>
<class>CashRebate</class>
<para>0.7</para>
</type>
</CashAcceptType>

实现的界面同之前一样


     

   “无论需求是什么,现在连程序都不动,只需要去改改XML文件就全部摆平。比如如果觉得现在满300

100 太多,要改成送 80,只需要去 XML文件里改就行,再比如希望增加新的算法,比如积分返点,那先写

一个返点的算法类继承 CashSuper,再去改一下 XML文件,对过去的代码依然不动。总之,现在是真的做到了程序易

维护,可扩展。”

  “知足是可以常乐,但知足如何能进步!你的代码真的没有问题了,比如说,你现在把列表是打印在了listBox

列表框中,我现在还需要输出到打印机打印成交易单据,我还希望这些清单能存入数据库中,你需要改客户端的代码

吗?”

  “这个,你这是加需求了,更改当然是必须的。”

  “更改是必须的没有错,但为什么我只是要对交易清单加打印和存数据,就需要去改客户端的代码呢?这两者

面向对象的四大好处--------- 可维护、可扩展、可复用和灵活性好

PC 易插拨的方式,那么不管哪一个出问题,都可以在不影响别的部件的前题下进行修改或替换。”

   “PC电脑里叫易插拨,面向对象里把这种关系叫什么?”

   “应该是叫强内聚、松耦合吧。”  

三层架构,分层开发


“那用 XML的配置文件就不合适了,应该用数据库会比较好!”

“那么老板听说了 C/S架构的坏处,更新麻烦,不够安全等等,他也不是傻瓜,每次更新都需要针对每台机器

部署,所以他提出要改为 B/S架构,客户端用浏览器支持,

“那需要改界面了,把应用程序改成 Web程序。”

“就现在的代码,改起来容易吗?”

“好象不容易,需要重新写,尽管可以复制一些代码过去,不过要重新写的东西还是很多的。”

“好,那有没有发现,这么多的需求变动,但系统中有一些东西一直没有变,是哪些?”

“策略模式用到的那几个类,也就是正常收费、打折消费、返利消费等算法是没有变化的。”

“其实不是算法不会变,而是之前我们已经考虑它很多了,用了策略模式,用了反射技术使得它的变化

了。总之,尽管三层架构不算难,不过由于现在很多数书籍材料的讲解不透,所以让我们初学者都概念模糊,理解有

误,非常的可惜的。

  “用来界面显示和处理的,对的,它们可以看作是一层。叫界面层?”

  “界面层这种叫法可以,或者叫UI层、表现层都可以。”

  “访问配置文件或处理数据库是不是就是数据层了?”

  “哈,三层架构是不是不难理解呀!说得很对,不过名称应该叫做数据访问层(DataAccess Layer)或简称 DAL

层。”

 “那么第三个层就是那些算法类了,这叫什么层呢?”

有了门面,程序员的程序会更加体面

 实际上没有学过设计模式去理解三层架构会有失偏颇的,毕竟分层是更高一级别的模式,所谓的架构

模式。不过在程序中,有意识的遵循设计原则,却也可以有效的做出好的设计。”

   “刚才讲的‘迪米特法则’就会在分层中用得上

1private void Form1_Load(object sender, EventArgs e)

2{

3//读配置文件

4ds = new DataSet();

ds.ReadXml(Application.StartupPath + "\\CashAcceptType.xml");

//将读取到的记录绑定到下拉列表框中

foreach (DataRowView dr in ds.Tables[0].DefaultView)

{

cbxType.Items.Add(dr["name"].ToString());

}

cbxType.SelectedIndex = 0;

}

private void btnOk_Click(object sender, EventArgs e)

{

    CashContext cc = new CashContext();

    //根据用户的选项,查询用户选择项的相关行

    DataRow dr = ((DataRow[])ds.Tables[0].Select("name='" + cbxType.SelectedItem.ToString()+"'"))[0];

    //声明一个参数的对象数组

 object[] args =null;

 //若有参数,则将其分割成字符串数组,用于实例化时所用的参数

 if (dr["para"].ToString() != "")

args = dr["para"].ToString().Split(',');

//通过反射实例化出相应的算法对象

cc.setBehavior((CashSuper)Assembly.Load("商 场 管 理 软 件").CreateInstance("商 场 管 理 软 件." +

dr["class"].ToString(),

false, BindingFlags.Default, null, args, null, null));

double totalPrices = 0d;

totalPrices = cc.GetResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));

total = total + totalPrices;

lbxList.Items.Add("单价:" + txtPrice.Text + "数量:"+ txtNum.Text + " "+cbxType.SelectedItem+ "

计:" + totalPrices.ToString());


 }

“这里 ,是为确定哪种算法而创建CashContext对象,其中用到了反射技术,为计算做准备

就想了了一个较好的办法,另一个设计模式,‘门面模式’(Facade)或叫外观模式


门面模式要求一个子系统的外部与其内部的通信必须通过一个统一的门面(Facade)对象进行。门面模式提供一个高层次

的接口,使得子系统更易于使用。

门面模式的结构

门面模式是对象的结构模式。门面模式没有一个一般化的类图描述,下图演示了一个门面模式的示意性对象图:

lblResult.Text = total.ToString();

在这个对象图中,出现了两个角色:

门面(Facade)角色:客户端可以调用这个角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常

情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去。

子系统(subsystem)角色:可以同时有一个或者多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。

每一个子系统都可以被客户端直接调用,或者被门面角色调用。子系统并不知道门面的存在,对于子系统而言,门面

仅仅是另外一个客户端而已。

DAL 层代码(目前是读配置文件,以后可以很容易的修改为访问数据库)

using System;

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()


通过我的读后感你是否对设计模式有了一定的了解呢!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值