深入浅出设计模式系列(二):策略模式
端午节到了,Jacky在家没事做,就跑去自己学长Cook的家串门。两个人聊得甚欢,Cook突然兴起,说,Jacky,今天我让你做个程序题,你想不想做。踌躇满志想成为一个优秀的程序员的Jacky当然毫不犹豫的就答应了。
Jacky:来吧,什么题目啊?没有难度的题目可别出哦!
Cook:你小子,长成了是吧?别看到时候出的题目简单,里面学问可不少呢。
Jacky:好吧,那你就别废话了,快说什么题目?
Cook:嗯,是这样的,就做一个商场的收营程序,程序要记录买的商品单价、商品数量,商品价格并且计算出商品的总价。
Jacky:就这个啊?那还不容易啊?不就是在一个窗体上放两个文本框分别用来填写商品单价、商品数量,然后用一个列表框来记录商品的清单,一个标签来记录总计,一个确定按钮来算出每种商品的费用。对了,还要一个重置按钮,来重新开始。
说着Jacky就去写代码了,不一会儿,半小时不到代码就写好了。他是这么写的
1 private void btnOK_Click(object sender, EventArgs e) 2 { 3 double total = 0.0d; 4 5 double totalPrice = Convert.ToDouble(txtProductNumber.Text) * Convert.ToDouble(txtProductUnitPrice.Text); 6 7 total += totalPrice; 8 9 lvProducts.Items.Add("商品数量:" + txtProductNumber.Text + " 商品单价:" + txtProductUnitPrice.Text + " 商品总价:" + totalPrice.ToString()); 10 11 lblTotal.Text += total.ToString(); 12 }
Cook看了看Jacky写的代码,摇了摇头对Jacky说
Cook:你看看,你这么写的话,如果我要现在又有一个新的需求,商店端午小长假促销活动,所有商品打8折,那你要怎么办啊?
Jacky:那也很简单啊,只有修改一行代码就行了啊,只要把代码改成下面那样就Ok了,这样就是打八折了。
1 double totalPrice = Convert.ToDouble(txtProductNumber.Text) * Convert.ToDouble(txtProductUnitPrice.Text) * 0.8;
Cook:那如果端午节过了呢?不打折了,你这套程序岂不是会让商场蒙受巨大的损失啊?
Jacky:那么到时候不打折了,我再把程序改回来就OK了啊。
Cook:现在一年四季那么多节日,打折促销活动又层出不穷的。一会儿打折让利,一会儿满300返100的活动让利,又一会儿消费满200积分50的活动,轮番上阵,那你岂不是得累死啊?跟着他们不停的修改程序,部署客户端,这样你不觉得麻烦吗?你不觉得麻烦,人家商场的领导都觉得麻烦了。
Jacky:啊。。。我好像是想的有些太简单了。那要不我增加一个下拉框,添加一些既定的打着方式,到时候让他们营业员来选择,这样就可以省掉一些麻烦事了。
就这样,Jacky又跑回电脑旁去改写自己刚才的程序,添加了一个让利下拉框,供客户程序进行选择。他是这么写的
1 private void btnOK_Click(object sender, EventArgs e) 2 { 3 double total = 0.0d; 4 double totalPrice = 0.0d; 5 6 switch (cmbType.SelectedText) 7 { 8 case "正常收费": 9 totalPrice = Convert.ToDouble(txtProductNumber) * Convert.ToDouble(txtProductUnitPrice); 10 break; 11 case "五折": 12 totalPrice = Convert.ToDouble(txtProductNumber) * Convert.ToDouble(txtProductUnitPrice) * 0.5; 13 break; 14 case "六折": 15 totalPrice = Convert.ToDouble(txtProductNumber) * Convert.ToDouble(txtProductUnitPrice) * 0.6; 16 break; 17 case "七折": 18 totalPrice = Convert.ToDouble(txtProductNumber) * Convert.ToDouble(txtProductUnitPrice) * 0.7; 19 break; 20 case "八折": 21 totalPrice = Convert.ToDouble(txtProductNumber) * Convert.ToDouble(txtProductUnitPrice) * 0.8; 22 break; 23 case "满300减100": 24 totalPrice = Convert.ToDouble(txtProductNumber) * Convert.ToDouble(txtProductUnitPrice) - Math.Floor(Convert.ToDouble(txtProductNumber) * Convert.ToDouble(txtProductUnitPrice) / 300) * 100; 25 break; 26 default: 27 break; 28 } 29 30 total += totalPrice; 31 32 lvProducts.Items.Add("商品数量:" + txtProductNumber.Text + " 商品单价:" + txtProductUnitPrice.Text + " 商品总价:" + totalPrice.ToString()); 33 34 lblTotal.Text += total.ToString(); 35 }
Jacky兴高采烈的跑去让Cook看他改进后的代码
Jacky:Cook,你快来,快来帮我看看,我把程序改进过了。
Cook:嗯,我看看,哦,这样子写的确是比先前的代码要灵活些了。不过还是不够好,你看看你的代码,打5折和打6折,7折,8折有什么区别?就是最后的乘数不同,你干嘛要写那么多重复的代码?看看能不能进行封装一下啊?看来我之前教你的,都白教了,你都给忘记了。
Jacky:哦哦,对哦,这个可以使用简单工厂方式,是吧?
一溜烟,Jacky又跑去电脑边写起了代码,很快他就写好了代码,拿来给Cook看,他是这么写的
1 //抽象产品类 2 public abstract class CashSuper 3 { 4 public abstract double AcceptCash(double money); 5 } 6 7 //正常收费产品类 8 public class CashNomal : CashSuper 9 { 10 public override double AcceptCash(double money) 11 { 12 return money; 13 } 14 } 15 16 //打折产品类 17 public class CashDiscount : CashSuper 18 { 19 public double Rate { get; set; } 20 21 public CashDiscount(double rate) 22 { 23 this.Rate = rate; 24 } 25 26 public override double AcceptCash(double money) 27 { 28 return money * Rate; 29 } 30 } 31 32 //返现产品类 33 public class CashBack : CashSuper 34 { 35 public double CashBackCondition { get; set; } 36 public double BackMoney { get; set; } 37 38 public CashBack(double cashBackCondition, double backMoney) 39 { 40 this.CashBackCondition = cashBackCondition; 41 this.BackMoney = backMoney; 42 } 43 44 public override double AcceptCash(double money) 45 { 46 return money - (money / CashBackCondition) * BackMoney; 47 } 48 } 49 50 //工厂类 51 public class PayMoneyFactory 52 { 53 public static CashSuper CreateAcceptCash(string payType) 54 { 55 CashSuper cashSuper = null; 56 57 switch (payType) 58 { 59 case "正常收费": 60 cashSuper = new CashNomal(); 61 break; 62 case "五折": 63 cashSuper = new CashDiscount(0.5); 64 break; 65 case "六折": 66 cashSuper = new CashDiscount(0.6); 67 break; 68 case "七折": 69 cashSuper = new CashDiscount(0.7); 70 break; 71 case "八折": 72 cashSuper = new CashDiscount(0.8); 73 break; 74 case "满300减100": 75 cashSuper = new CashBack(300, 100); 76 break; 77 case "满200减50": 78 cashSuper = new CashBack(200, 50); 79 break; 80 } 81 82 return cashSuper; 83 } 84 } 85 86 //客户端代码 87 private void btnOK_Click(object sender, EventArgs e) 88 { 89 double total = 0.0d; 90 double totalPrice = 0.0d; 91 92 CashSuper cashSuper = PayMoneyFactory.CreateAcceptCash(cmbType.SelectedText); 93 94 totalPrice = cashSuper.AcceptCash(Convert.ToDouble(txtProductNumber.Text) * Convert.ToDouble(txtProductUnitPrice.Text)); 95 96 total += totalPrice; 97 98 lvProducts.Items.Add("商品数量:" + txtProductNumber.Text + " 商品单价:" + txtProductUnitPrice.Text + " 商品总价:" + totalPrice.ToString()); 99 100 lblTotal.Text += total.ToString(); 101 }
Cook:嗯,我看看代码,Jacky写的不错啊,有长进,看来现在你对简单工厂模式掌握的不错啊。。。
Jacky:那是当然的,你也不看看我是谁?
Cook:好了,别又得意忘形了。简单工厂模式虽然也能解决这个问题,但这个模式只能解决对象的创建问题,而且由于工厂本身包括了所有的收费方式,商场可能经常性的更改打折额度,每次维护和扩展收费方式都要改动这个工厂,以致于代码需要重新编译部署,这真是很糟糕的处理方式,所以用它不是最好的办法。面对算法的时常变动,应该可以有更好的办法。今天我就教你一个新招。
Jacky:又是什么新招?什么设计模式?你就别再卖关子了,快告诉我吧。
Cook:嗯,那就是策略模式。来,看我来给你写代码。
1 //抽象策略角色 2 public abstract class CashSuper 3 { 4 public abstract double AcceptCash(double money); 5 } 6 7 //正常收费策略 8 public class CashNomal : CashSuper 9 { 10 public override double AcceptCash(double money) 11 { 12 return money; 13 } 14 } 15 16 //打折策略 17 public class CashDiscount : CashSuper 18 { 19 public double Rate { get; set; } 20 21 public CashDiscount(double rate) 22 { 23 this.Rate = rate; 24 } 25 26 public override double AcceptCash(double money) 27 { 28 return money * Rate; 29 } 30 } 31 32 //返现策略 33 public class CashBack : CashSuper 34 { 35 public double CashBackCondition { get; set; } 36 public double BackMoney { get; set; } 37 38 public CashBack(double cashBackCondition, double backMoney) 39 { 40 this.CashBackCondition = cashBackCondition; 41 this.BackMoney = backMoney; 42 } 43 44 public override double AcceptCash(double money) 45 { 46 return money - (money / CashBackCondition) * BackMoney; 47 } 48 } 49 50 //环境角色,上下文,持有一个策略类的引用,供最终给客户端调用 51 public class CashContext 52 { 53 private CashSuper cashSuper = null; 54 55 public CashContext(string payType) 56 { 57 switch (payType) 58 { 59 case "正常收费": 60 cashSuper = new CashNomal(); 61 break; 62 case "五折": 63 cashSuper = new CashDiscount(0.5); 64 break; 65 case "六折": 66 cashSuper = new CashDiscount(0.6); 67 break; 68 case "七折": 69 cashSuper = new CashDiscount(0.7); 70 break; 71 case "八折": 72 cashSuper = new CashDiscount(0.8); 73 break; 74 case "满300减100": 75 cashSuper = new CashBack(300, 100); 76 break; 77 case "满200减5100": 78 cashSuper = new CashBack(200, 50); 79 break; 80 } 81 } 82 83 public double GetPayResult(double money) 84 { 85 return cashSuper.AcceptCash(money); 86 } 87 } 88 89 //客户端代码 90 private void btnOK_Click(object sender, EventArgs e) 91 { 92 double total = 0.0d; 93 double totalPrice = 0.0d; 94 95 CashContext context = new CashContext(cmbType.SelectedText); 96 totalPrice = context.GetPayResult(Convert.ToDouble(txtProductNumber.Text) * Convert.ToDouble(txtProductUnitPrice.Text)); 97 98 total += totalPrice; 99 100 lvProducts.Items.Add("商品数量:" + txtProductNumber.Text + " 商品单价:" + txtProductUnitPrice.Text + " 商品总价:" + totalPrice.ToString()); 101 102 lblTotal.Text += total.ToString(); 103 }
Cook:看像这样商场收营如何促销,是打折还是返利,其实都是一些算法,算法本身只是一个策略,最重要这些算法随时都可能互相替换的,这就是变化点,封装变化点就是我们面向对象的一种重要的思维方式。这里有一个抽象类,这个抽象类定义了一个获取收营金额的方法,然后不同的策略算法类来继承自这个抽象类,实现这个抽象类的抽象方法计算收营金额,在CashContext类中,维护一个收营的父类对象,根据收营方式的不同,动态的调用不同策略类计算收营的方法。
策略模式(Strategy Pattern)
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。(原文:The Strategy Pattern defines a family of algorithms,encapsulates each one,and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.)
Context(应用场景):
- 需要使用ConcreteStrategy提供的算法。
- 内部维护一个Strategy的实例。
- 负责动态设置运行时Strategy具体的实现算法。
- 负责跟Strategy之间的交互和数据传递。
Strategy(抽象策略类):
- 定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,Context使用这个接口调用不同的算法,一般使用接口或抽象类实现。
ConcreteStrategy(具体策略类):
- 实现了Strategy定义的接口,提供具体的算法实现。
策略模式的组成
- 抽象策略角色: 策略类,通常由一个接口或者抽象类实现。
- 具体策略角色:包装了相关的算法和行为。
- 环境角色:持有一个策略类的引用,最终给客户端调用。
策略模式的应用场景
- 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。(例如FlyBehavior和QuackBehavior)
- 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。(例如FlyBehavior和QuackBehavior的具体实现可任意变化或扩充)
- 对客户(Duck)隐藏具体策略(算法)的实现细节,彼此完全独立。
策略模式的优点
- 提供了一种替代继承的方法,而且既保持了继承的优点(代码重用)还比继承更灵活(算法独立,可以任意扩展)。
- 避免程序中使用多重条件转移语句,使系统更灵活,并易于扩展。
- 遵守大部分GRASP原则和常用设计原则,高内聚、低偶合。
策略模式的缺点
- 由于每个具体策略类都会产生一个新类,所以会增加系统需要维护的类的数量
文章声明:本文部分内容参考自《大话设计模式》,这是一本学习设计模式非常好的书。