1、第二讲,策略模式,商场促销,大鸟:“吼吼,记住了,编程是一门技术,更是一门艺术,不能只满足于写完代码运行结果正确就完事,时常考虑如何让代码更加简炼,更加容易维护,容易扩展和复用,只有这样才可以是真的提高。写出优雅的代码真的是一种很爽的事情。不过学无止境,其实这才是理解面向对象的开始呢。给你出个作业:做一个商场收银软件,营业员根据客户购买商品单价和数量,向客户收费。” 小菜:“就这个?没问题呀。” 小菜心里想:“大鸟要我做的是一个商场收银软件,营业员根据客户购买商品单价和数量,向客户收费。这个很简单,两个文本框,输入单价和数量,再用个列表框来记录商品的合计,最终用一个按钮来算出总额就可,对,还需
2、要一个重置按钮来重新开始,不就行了?!”,地点:大鸟房间 人物:小菜、大鸟 时间:某晚10点,小菜的初版关键代码,商场收银系统v1.0关键代码如下:/声明一个double变量total来计算总计 double total = 0.0d; private void btnOk_Click(object sender, EventArgs e) double totalPrices=Convert.ToDouble(txtPrice.Text) *Convert.ToDouble(txtNum.Text); total = total + totalPrices; lbxList.Items.Ad
3、d(“单价:”+txtPrice.Text+“ 数量:“+txtNum.Text+“ 合计:“+totalPrices.ToString(); lblResult.Text = total.ToString(); ,需求稍变如何?,“大鸟,”小菜叫道,“来看看,这不就是你要的收银软件吗?我不到半小时就搞定了。” “哈哈,很快嘛,”大鸟说着,看了看小菜的代码。接着说:“现在我要求商场对商品搞活动,所有的商品打8折。” “那不就是在totalPrices后面乘以一个0.8吗?” “小子,难道商场活动结束,不打折了,你还要再改写一遍程序代码,再去把所有机器全部安装一次吗?再说,我现在还有可能因为周年
4、庆,打五折的情况,你怎么办?” 小菜不好意思道:“啊,我想得是简单了点。其实只要加一个下拉选择框就可以解决你说的问题。” 大鸟微笑不语。,商场收银系统v1.1关键代码,double total = 0.0d; /总计private void form1_Load(object sender, EventArgs e) cbxType.Items.Addrange(new object “正常收费”, “8折”, “7折”);cbxType.SelectedIndex=0; /初始化折扣下拉框private void btnOk_Click(object sender, EventArgs e)
5、 double totalPrices=0d;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)
6、*Convert.ToDouble(txtNum.Text)*0.7; break;total = total + totalPrices; lbxList.Items.Add(“单价:”+txtPrice.Text+“ 数量:“+txtNum.Text+“ 合计:“+totalPrices.ToString(); lblResult.Text = total.ToString(); ,再添点需求如何?,“这下可以了吧,只要我事先把商场可能的打折都做成下拉选择框的项,要变化的可能性就小多了。”小菜说道。 “这比刚才灵活性上是好多了,不过重复代码很多,像Convert.ToDouble(),你这
7、里就写了6遍,而且3个分支要执行的语句除了打折多少以外几乎没什么不同,应该考虑重构一下。不过还不是最主要的,现在我的需求又来了,商场的活动加大,需要有满300返100的促销算法,你说怎么办?” “满300返100,那要是700就要返200了?这个必须要写函数了吧?” “小菜呀,看来之前教你的白教了,这里面看不出什么名堂吗?”,简单工厂模式能解决一切?,“哦!我想起来了,你的意思是简单工厂模式是吧,对的对的,我可以先写一个父类,再继承它实现多个打折和返利的子类,利用多态,完成这个代码。” “你打算写几个子类?” “根据需求呀,比如8折、7折、5折、满300送100、满200送50要几个写几个。”
8、 “小菜又不动脑子了,有必要这样吗?如果我现在要3折,我要满300送80,你难道再去加子类?你不想想看,这当中哪些是相同的,哪些是不同的?” “ 对的,这里打折基本都是一样的,只要有个初始化参数就可以了。满几送几的,需要两个参数才行,明白,现在看来不麻烦了。”,学会抽象!,“面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类 。打一折和打九折只是形式的不同,抽象分析出来,所有的打折算法都是一样的,所以打折算法应该是一个类。好了,空话已说了太多,写出来才是真的懂。” 大约1个小时后,小菜交出了第三份的作业,代码结构图,?,各类的具体
9、实现,现金收费抽象类(CashSuper) abstract class CashSuperpublic abstract double acceptCash(double money); 正常收费子类(CashNormal) class CashNormal :CashSuperpublic override double acceptCash(double money)return money; ,正常收费,原价返回,抽象方法,收取现金,参数是原价,返回为当前价,打折收费子类,class CashRebate : CashSuperprivate double moneyRebate=1d
10、;public CashRebate(String moneyRebate)this.moneyRebate=double.parse(moneyRebate);public override double acceptCash(double money)return money*moneyRebate; ,打折收费,必须输入初始折扣率,返利收费子类,class CashReturn : CashSuperprivate double moneyCondition=0.0d;private double moneyReturn=0.0d;public CashReturn(String mon
11、eyCondition,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; ,返利收费,必须输入两
12、个初始参数:返利条件和返利值,如满300送100,若大于返利条件,则需减去返利值,现金收费工厂类,class CashFactorypublic static CashSuper createCashAccept (String type)CashSuper cs=null;switch(type)case “正常收费”:cs=new CashNormal(); break;case “满300送100”:cs=new CashReturn(); break;case “8折”:cs=new CashRebate(); break;return cs; ,现金收费工厂,根据条件返回相应收费对象
13、,客户端程序主要部分,double total = 0.0d; /声明一个double变量total来计算总计 private void btnOk_Click(object sender, EventArgs e) CashSuper cs;cs=CashFactory.createCashAccept( cbxType.SelectedItem.toString();double totalPrices=cs.acceptCash(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text); total = total +
14、 totalPrices; lbxList.Items.Add(“单价:”+txtPrice.Text+“ 数量:” +txtNum.Text+ “ ”+cbxType.SelectedItem+“ 合计:“+totalPrices.ToString(); lblResult.Text = total.ToString(); ,你能让客户不提要求吗?,“大鸟,搞定,这次无论你要怎么改,我都可以简单处理就行了。”小菜自信满满的说。 “是吗,要是需要打5折和满500送200的促销活动,如何办?” “只要在收费对象生成工厂当中加两个条件,在界面的下拉选项框里加两项,就OK了。” “说得不错,如果我现
15、在需要增加一种商场促销手段,满100积分10点,以后积分到一定时候可以领取奖品如何做?” “有了工厂,何难?加一个积分算法类,构造方法有两个参数:条件和返点,让它继承CashSuper,再到收费对象生成工厂里加满100积分10点的分支条件,再到界面稍加改动,就行了。”,你能让客户不提要求吗?,“嗯,不错,那我问你,如果商场现在需要拆迁,没办法,只能跳楼价销售,商场的所有商品都需要打8折,打折后的价钱再每种商品满300送50,最后计总价的时候,商场还满1000送200,你说如何办?” “搞没搞错哦,这商场不如白送得了,哪有这样促销的?老板跳楼时估计都得赤条条的了。” “商场大促销你还不高兴呀!当
16、然,你是软件开发者,客户老是变动需求的确不爽,但你不能不让客户提需求呀,我不是说过吗,需求的变更是必然!所以开发者应该做的是考虑如何让自己的程序更能适应变化,而不是抱怨客户的无理,客户不会管程序员加班时的汗水,也不相信程序员失业时的眼泪,因为客户自己正在为自己的放血甩卖而流泪呀。”,老革命遇到了新问题,“你对简单工厂用得很熟练了嘛。”大鸟接着说:“简单工厂模式虽然也能解决这个问题,但这个模式只是解决对象的创建问题。并且由于工厂本身包括了所有的收费方式,商场是可能经常性的更改打折额度和返利额度,每次维护或扩展收费方式都要改动这个工厂,以致代码需要重新编译部署,这真的是很糟糕的处理方式,所以它不是
17、最好的办法。面对算法的时常变动,应该有更好的办法。好好去研究一下设计模式吧,就会有解决办法的。 小菜进入了沉思中,策略模式,小菜次日来找大鸟,说:“我找到相关设计模式了,应该是策略模式(Strategy)。策略模式:它定义了算法家族,分别封装起来,让它们之间可以互相替换, 此模式让算法的变化,不会影响到使用算法的客户。看来商场收银系统应该考虑用策略模式?” “你问我?你说呢?”大鸟笑道,“商场收银时如何促销,用打折还是返利,其实都是一些算法,用工厂来生成算法对象,感觉是不是很怪?而最重要的是这些算法是随时都可能互相替换的,这就是变化点,而封装变化点是我们面向对象的一种很重要的思维方式。我们来看
18、看策略模式的结构图和基本代码。”,策略模式的结构图,策略类,定义所有 支持的算法的公共 接口,具体策略类,封装了具体的算法或行为,继承自Strategy,Context上下文,用一个 ConcreteStrategy来配置, 维护一个对Strategy对象 的引用,基本代码,Strategy类,定义所有支持的公共接口/抽象算法类abstract class Strategy/算法方法public abstract void AlgorithmInterface(); ,ConcreteStrategy类,/封装了具体的算法或行为,继承自Strategy class ConcreteStrate
19、gyA : Strategypublic override void AlgorithmInterface()Console.WriteLine(“算法A的实现”); class ConcreteStrategyB : Strategypublic override void AlgorithmInterface()Console.WriteLine(“算法B的实现”); class ConcreteStrategyC : Strategypublic override void AlgorithmInterface()Console.WriteLine(“算法C的实现”);,Context
20、上下文类,/用一个具体策略类来配置,维护对抽象策略类对象的引用 class Context /上下文Strategy strategy;public Context(Strategy strategy)this.strategy= strategy;/上下文接口public void contextInterface()strategy.AlgorithmInterface();,用具体策略对象初始化,根据具体策略对象,调用其算法的方法,客户端代码,static void main(String args)Context contex;context= new Context(new Conc
21、reteStrategyA();context.contextInterface();context= new Context(new ConcreteStrategyB();context.contextInterface();context= new Context(new ConcreteStrategyC();context.contextInterface();Console.Read(); ,实例化不同策略,调用接口算法时,结果不同,策略模式实现收费系统,“我明白了,”小菜说,“我昨天写的CashSuper就是抽象策略,而正常收费CashNormal、打折收费CashRebate和
22、返利收费CashReturn就是三个具体策略,也就是策略模式中说的具体算法,对吧?” “是的,那么关键就在于Context以及客户端程序如何写了?去查查资料,研究后把代码写出来给我看。”大鸟鼓励道。 “好的,我一定很快写出来给你看!”小菜很兴奋。,收银系统V1.2 代码结构图,CashContext类,class CashContext CashSuper cs; /声明一个CashSuper对象public CashContext(CashSuper csuper)this.cs= csuper; /通过构造方法,传入具体收费策略public double getResult(double
23、money)return cs.acceptCash(money); /根据不同策略获得结果,客户端主要代码,double total = 0.0d; /声明一个double变量total来计算总计 private void btnOk_Click(object sender, EventArgs e) CashContext cc;switch(cbxType.selectedItem.toString() case “正常收费”:cc=new CashContext(new CashNormal(); break;case “满300送100”:cc= new CashContext(ne
24、w CashReturn(); break;case “8折”:cc=new CashContext(new CashRebate(); break;double totalPrices=cc.getResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text); total = total + totalPrices; lbxList.Items.Add(“单价:”+txtPrice.Text+“ 数量:” +txtNum.Text+ “ ”+cbxType.SelectedItem+“ 合计:“+totalPri
25、ces.ToString(); lblResult.Text = total.ToString(); ,由Context获得结果,隔离了客户和具体算法,根据下拉选择框,使用不同策略初始化CashContext,不觉得有点别扭吗?,“大鸟,我用策略模式是实现了,但我感觉这样子做不又回到原来的老路了吗,在客户端去判断用哪一个算法?这等于要改变需求算法时,还是要去更改客户端的程序呀?” “是的,但是你有什么好办法,把这个判断的过程从客户端程序转移走呢?” “转移走?不明白,原来用简单工厂是可以做到的,现在这样子如何做到?” “难道简单工厂一定要是一个单独的类吗?难道不可以与策略模式的Context类
26、结合?” “哦,我明白你的意思了,let me try。”,策略与简单工厂结合,class CashContext /改造CashContextCashSuper cs=null; /声明一个CashSuper对象public CashContext(String type)switch(type ) case “正常收费”:cs=new CashNormal(); break;case “满300送100”:cs= new CashReturn(); break;case “8折”:cs=new CashRebate(); break;public double getResult(doub
27、le money)return cs.acceptCash(money); /根据不同策略获得结果,通过构造方法,传入收费类型字符串,而不是具体收费策略对象,把实例化具体策略的过程由客户端转移到Context类中。简单工厂的应用,改造后客户端主要代码,double total = 0.0d; private void btnOk_Click(object sender, EventArgs e) CashContext cc=new CashContext (cbxType.selectedItem.toString();double totalPrices=cc.getResult(Conv
28、ert.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(); ,根据下拉选择框,将算法类型字符串传入CashContext对象中,简单工厂与策略模式之异同,“改进后的客户端和你写的简
29、单工厂的客户端比呢?观察一下,找出它们的不同之处。” /简单工厂模式的用法 CashSuper cs=CashFactory.createCashAccept(cbxType.selectedItem.toString(); = cs.getResult();/策略模式与简单工厂结合的用法 CashContext cs=new CashContext(cbxType.selectedItem.toString(); = cs.getResult();,“你的意思是说,简单工厂模式我需要客户端认识两个类,CashSuper和CashFactory,而策略模式和简单工厂结合的用法,客户端只需要认识
30、一个类CashContext就可以了。耦合更加降低。” “说的没错,我们在客户端实例化的是CashContext对象,调用的是这个对象的方法getResult(),这使得具体的收费算法彻底与客户端分离。连算法的父类CashSuper都不让客户端认识了。”,策略模式解析,“回过头来反思一下策略模式,策略模式是一种定义一系列算法的方法,从概念是上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式来调用所有的算法,减少了各种算法类与使用算法类之间的耦合。”大鸟总结道。 “策略模式还有些什么优点?”小菜问道。 “策略模式的Strategy类层次为Context定义了一些列的可供重
31、用的算法或行为。继承有助于析取出这些算法中的公共功能。针对我们的应用,你说这公共功能是什么?” “公共功能就是获得计算费用的结果getResult,这是的算法之间就有了抽象的父类。”,“对,很好。另一个策略模式的优点是简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。” “每个算法都可以保证它没有错误,修改其中任何一个时都不影响其它算法。这真的是非常好!” “哈,小菜今天表现不错,我所想的你都想到了。”大鸟表扬道,“另外,在开始编程时,你不得不在客户端代码中为决定用哪个算法而用了switch分支,这很正常。因为,当不同的行为堆砌到一个类中时,就很难避免使用条件语句来选择合适
32、的行为。把这些行为封装在一个个独立的Strategy类中,可以在使用这些行为的类中消除条件语句。就前面的收银系统来说,在客户端代码中就消除了条件分支,避免了大量的判断。这是个重要的进展。你能用一句话来概括这个优点吗?”大鸟反问。 “策略模式封装了变化。”小菜快速而坚定地说。,“说得好,策略模式就是用来封装算法的。但在实践中,我们发现可以用它来封装任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑用策略模式来处理这种变化的可能性。” “但我感觉在基本策略模式中,选择所用具体实现的职责由客户端对象承担,并转给策略模式的Context对象。这并未解除客户端需要选择判断的压力,而策略模式与简单工厂结合后,选择具体实现的职责也可由Context来承担,这就大大减轻了客户端的职责。” “是的,这比起初的策略模式已经好用了,不过它依然不够完美。” “哦,还有不足?” “因为在CashContext中还是用到了switch,假如再增加一种算法,如满200送50,就必须得改CashContext中的switch代码,总让人不爽呀。” “那怎么办?”,