版权声明:本文为博主ExcelMann的原创文章,未经博主允许不得转载。
第2章 策略模式
作者:ExcelMann,转载需注明。
正文
本章介绍的是【策略模式】,要实现的需求是:做一个商场收银软件,营业员根据客户所购买商品的单价和数量,向客户收费。
关键知识:
- 策略模式的介绍
- 策略模式和简单工厂模式的区别
- 策略模式和简单工厂模式的结合
备注:文章中的有关于界面的代码使用C#语言实现。
1.V1.0版本
需求:用两个文本框来输入单价和数量,一个确定按钮来算出每种商品的费用,用个列表框来记录商品的清单,一个标签来记录总价,还有一个下拉选择框来选择打折的方式。
代码如下:
double total = 0.0d;
private void Form1_Load(object sender,EventArgs e)
{
cbxType.Items.AddRange(new Object[] {"正常收费","打8折","打7折","打5折"});
cbxType.SelectedIndex = 0;
}
private void btnOk_Click(object sender, EventArgs e)
{
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)*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("....");
...
}
该代码出现的问题是:
- 重复代码很多!比如,Convert.ToDouble代码就出现了8次,如果再加一种新的情况,就还要再重复写2次,造成不可复用的问题;
- 各个case分支中的代码,除了打折,其他代码没有区别;
- 面对新的需求,又得是要拿到这个类,然后更新其中的代码,这造成维护性低的问题;
- 个人理解:业务逻辑代码和客户端(界面)代码耦合性太高。造成不可复用、维护性低、拓展性低、不够灵活的问题;
解决方法:利用简单工厂模式(封装+继承+多态)。
2.开始使用简单工厂模式
初始想法:
先写一个父类,再继承它实现多个打折和返利的子类,利用多态,完成这个代码。
问题所在:
需要写多少个子类?对于每个打折和返利都需要写一个子类吗?
关键:
在封装的过程中,若要分类,要找出其中的不同和相同点。封装的关键是抽象,即找出具有相似属性和功能的对象集合作为类。
在这里来分析的话,就是对于每个打折的类,抽象出来为"打(?)折类",对于每个返利的类,抽象出来为"达到(?)元返利(?)元的类"。
实现:
public abstract class CaseSuper
{
public abstract double acceptCash(double money);
}
public class CaseNormal extends CashSuper
{
public double acceptCash(double money)
{
return money;
}
}
public class CashRebate extends CashSuper
{
private double moneyRebate = 1d;
public CashRebate(String moneyRebate)
{
this.moneyRebate = moneyRebate;
}
public double acceptCash(double money)
{
return money * moneyRebate;
}
}
// ...其他子类
// 工厂类
public class CashFactory
{
public static CashSuper createCashAccept(String type)
{
CashSuper cs = null;
switch(type){
case "正常收费":
cs = new CashNormal();
break;
// ...其他case
}
return cs;
}
}
现在,已经实现了可维护(修改其中一个CashSuper子类代码,不影响其他类)、可拓展(可以通过增加新的子类继承CashSuper,来增加新的计算方式)、可复用(这些子类都可以用于其他的情形当中,而不用写重复代码)、灵活性高。
新的问题:
简单工厂模式虽然可以解决这个问题,但是工厂模式的目的还是用于解决对象的创建问题。如果每次要增加新的收费方式,或者维护旧的收费方式,都得要更新工厂方法,以致代码需要重新编译部署。
所以面对算法的时常变动,应该有更好的方法:策略模式。
3.策略模式的介绍
定义:策略模式定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。
结构图和代码:
// 抽象算法类
public abstract class Strategy
{
// 算法的共同方法
public abstract void AlgorithmInterface();
}
// 具体算法A
public class ConcreteStrategyA extends Strategy
{
// 实现算法的方法
public void AlgorithmInterface()
{
...
}
}
// ...其他具体实现算法
// Context上下文
public class Context
{
private Strategy strategy;
public Context(Strategy strategy) // 构造对象时,就指定实现的具体算法策略
{
this.strategy = strategy;
}
public void ContextInterface()
{
strategy.AlgorithmInterface(); // 调用指定策略算法的方法
}
}
关键点:
我认为策略模式的关键点在于,抽象策略类抽象了这类策略的方法,而各个子策略通过不同的代码来实现该方法。对于Context上下文,负责切换各个算法,但是Context中的ContextInterface
方法,调用的方法永远是抽象策略类所抽象的那个方法。
书中原话:
对于打折还是返利,其实这些都是算法,用工厂方法来生成算法对象本身没有错,但是算法本身是一种策略。最重要的是这些算法是随时都可能互相替换的,这就是变化点,而封装变化点是我们面向对象的一种很重要的思维方式。
对于这句话我的理解是:要弄清楚工厂模式和策略模式之间的区别。
- 从刻板的文字理解上分析:工厂模式主要解决的是对象创建的问题,但不是所有对象都是算法/策略;策略模式主要解决的是相似算法切换的问题,对于上述的那些算法(打折、返利),由父类来抽象出共同的方法,然后由Context上下文来切换不同的算法;
- 从实际应用上分析:如果使用工厂模式来达到策略模式的效果,那么你在客户端中,对于每个工厂模式获得的算法对象,调用算法方法的代码中,得确保调用的方法都是同一个;而对于策略模式,其在Context上下文中,通过一个
ContextInterface
方法就控制好了各个算法将要调用的方法,保证各个算法调用的方法都是为了同一个需求; - 或者换个说法:工厂模式只是一个生产对象的工厂类,具体对对象做什么操作由客户端代码来决定;而策略模式在Context中管理对象,对象的操作都是由Context统一管理;
4.开始使用策略模式
商场收银系统新版本,代码结构图:
代码:
// CashContext类
public class CashContext
{
private CashSuper cs;
public CashContext(CashSuper cs) // 通过构造方法,设定具体算法策略
{
this.cs = cs;
}
public double getResult(double money){
return cs.acceptCash(money); // 调用指定算法策略的该方法,计算Cash
}
}
客户端主要代码(C#实现)
double total = 0.0d;
private void btnOk_Click(object sender, EventArgs e)
{
CashContext cc = null;
switch(cbxType.SelectedItem.ToString())
{
case "正常收费":
cc = new CashContext(new CashNormal()); // 通过构造方法的参数,指定具体策略算法
break;
// ...其他case
}
double totalPrices = 0d;
// 调用cc.getResult()方法,可以得到收取费用的结果,让具体算法的实现与客户进行了隔离
totalPrices = cc.getResult(Convert.ToDouble(txtPrice.Text)*Convert.ToDouble(txtNum.Text));
total = total + totalPrices;
// ...其余界面更新数据代码
}
新的问题:
switch判断的过程又是在客户端代码中出现。如何将客户端代码中的switch判断过程转移到其他地方呢?
答案是:采用简单工厂模式。
不过,这里的简单工厂模式不是一个单独的类来管理,而是借助策略模式的Context来管理。(简单工厂模式的另一种做法)
5.将策略和简单工厂模式结合
改造后的CashContext:
public class CashContext
{
CashSuper cs = null;
// 将switch过程转移到CashContext的构造方法中。简单工厂的应用
public CashContext(String type)
{
switch(type){
case "正常收费":
cs = new CashNormal();
break;
// ...其他case
}
}
public double getResult(double money)
{
return cs.acceptCash(money);
}
}
客户端代码:
double total = 0.0d;
private void btnOk_Click(object sender, EventArgs e)
{
// 将之前的switch过程,化简为一个语句
CashContext cc = new CashContext(cbxType.SelectedItem.ToString());
double totalPrices = 0d;
// 调用cc.getResult()方法,可以得到收取费用的结果,让具体算法的实现与客户进行了隔离
totalPrices = cc.getResult(Convert.ToDouble(txtPrice.Text)*Convert.ToDouble(txtNum.Text));
total = total + totalPrices;
// ...其余界面更新数据代码
}
6.策略模式的总结解析
原文句子:
- 首先,反思一下策略模式,策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。
我的理解:跟我在上面提到的策略模式和简单工厂模式的区别中提到的类似。 - 策略模式的另一个优点是:简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。
- 策略模式封装了变化:当不同的行为堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为。将这些行为封装在一个个独立的Strategy类中,可以在使用这些行为的类中消除条件语句。
- 策略模式就是用来封装算法的,但在实践中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
- 不足之处:在Context中还是使用了switch语句,不过其实可以使用反射机制来解决。在后续的抽象工厂模式中会提到。