首页 > 代码库 > 策略模式
策略模式
商场促销
现在小菜被要求做一个商场促销软件,界面如下所示。
功能描述:
输入每件商品的单价和数量,点击确定计算该种类商品的总价值。
把商品单价、数量、总价值显示到一个ListBox中。
在最下面显示此次购物的总交费额。
点击重置,清除所有的数据。总计显示为0。
小菜上来就写,实现的代码如下:
public partial class Form1 : Form { double totalCash = 0; public Form1() { InitializeComponent(); } private void btnOK_Click(object sender, EventArgs e) { double sumPrice = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtQuantity.Text); //listbox显示每件商品的价钱 lbxList.Items.Add("单价: " + txtPrice.Text + " 数量: " + txtQuantity.Text + " 合计: " + sumPrice.ToString()); //lblResult中显示总价钱 totalCash += sumPrice; lblResult.Text = totalCash.ToString(); } private void btnReset_Click(object sender, EventArgs e) { totalCash = 0; txtPrice.Text = ""; txtQuantity.Text = ""; lblResult.Text = totalCash.ToString(); //清除ListBox的内容 lbxList.Items.Clear(); } }
但是现在面临一个问题,就是商场要经常打折,不能说是没次打折都要修改totalCash,给他乘以一个系数。小菜的解决办法是增加一个combox下拉列表框。
public partial class Form1 : Form { double totalCash = 0; public Form1() { InitializeComponent(); } private void btnOK_Click(object sender, EventArgs e) { double sumPrice = 0; switch(cbxType.SelectedIndex) { case 0: sumPrice = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtQuantity.Text); break; case 1: sumPrice = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtQuantity.Text) * 0.8; break; case 2: sumPrice = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtQuantity.Text) * 0.7; break; case 3: sumPrice = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtQuantity.Text) * 0.5; break; default: break; } totalCash += sumPrice; //listbox显示每件商品的价钱 lbxList.Items.Add("单价: " + txtPrice.Text + " 数量: " + txtQuantity.Text + " "+ cbxType.SelectedItem + " 合计: " + sumPrice.ToString()); //lblResult中显示总价钱 lblResult.Text = totalCash.ToString(); } private void btnReset_Click(object sender, EventArgs e) { totalCash = 0; txtPrice.Text = ""; txtQuantity.Text = ""; lblResult.Text = totalCash.ToString(); //清除ListBox的内容 lbxList.Items.Clear(); cbxType.SelectedIndex = 0; } private void Form1_Load(object sender, EventArgs e) { cbxType.Items.AddRange(new object[] {"正常收费", "打八折", "打七折", "打五折"}); cbxType.SelectedIndex = 0; } }
这样写确实比原来灵活了好多,但是重复的代码比较多,Convert.ToDouble()就写了好多遍,而且四个分支要执行的语句几乎没什么不同。所以要考虑一下重构。
不过最主要的东西来了,现在商场活动加大,需要有满300返100的促销算法,菜鸟一般会直接加一个实现其功能的函数。大鸟的做法就是用简单工厂模式了,先写一个父类,再继承它实现多个打折和返利的子类,利用多态,完成这个代码。这里要找出哪里是相同的,哪里是不同的,不能说八折写个子类,七折写个子类…满200返50写个子类…,这样的话就真的成菜鸟了。
这里的打折都是一样的,只要有个初始化的参数就可以了。满几送几的,需要两个参数才行。
面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。
现金收费抽象类:
abstract class CashSuper { //接收参数money:当前价格 //函数返回值:活动时期的当前价格 public abstract double AcceptCash(double money); }
正常收费子类:
class CashNormal : CashSuper { public override double AcceptCash(double money) { return money; } }
打折收费子类:
class CashRebate : CashSuper { private double moneyRebate = 1; //既然这个类是实现打折的算法,那么在类的构造的时候就要知道打几折 public CashRebate(string moneyRebate) { this.moneyRebate = double.Parse(moneyRebate); } public override double AcceptCash(double money) { return money * moneyRebate; } }
返利收费子类:
class CashReturn : CashSuper { private double moneyConditon = 0; private double moneyReturn = 0; //既然这个类是实现返现的算法,那么在类的构造的时候就要知道返现的规则 public CashReturn(string moneyCondition, string moneyReturn) { this.moneyConditon = double.Parse(moneyCondition); this.moneyReturn = double.Parse(moneyReturn); } public override double AcceptCash(double money) { double countPoint = Math.Floor(money / moneyConditon); money -= countPoint * moneyReturn; return money; } }
现金收费工厂类:
class CashFactory { public static CashSuper CreateCashAccept(string type) { CashSuper cs = null; switch(type) { case "正常收费": cs = new CashNormal(); break; case "打8折": cs = new CashRebate("0.8"); break; case "满300返100": cs = new CashReturn("300", "100"); break; default : break; } return cs; } }
客户端程序主要部分:
public partial class Form1 : Form { double totalCash = 0; public Form1() { InitializeComponent(); } private void btnOK_Click(object sender, EventArgs e) { double sumPrice = 0; //计算一下该中商品的总价值 sumPrice = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtQuantity.Text); //利用工厂生产价格计算类,并调用相应的算法 CashSuper cs = CashFactory.CreateCashAccept(cbxType.SelectedItem.ToString()); //更新sumPrice的值,打折或者返现时的值 sumPrice = cs.AcceptCash(sumPrice); totalCash += sumPrice; //listbox显示每件商品的价钱 lbxList.Items.Add("单价: " + txtPrice.Text + " 数量: " + txtQuantity.Text + " "+ cbxType.SelectedItem + " 合计: " + sumPrice.ToString()); //lblResult中显示总价钱 lblResult.Text = totalCash.ToString(); } private void btnReset_Click(object sender, EventArgs e) { totalCash = 0; txtPrice.Text = ""; txtQuantity.Text = ""; lblResult.Text = totalCash.ToString(); //清除ListBox的内容 lbxList.Items.Clear(); cbxType.SelectedIndex = 0; } private void Form1_Load(object sender, EventArgs e) { cbxType.Items.AddRange(new object[] {"正常收费", "打8折", "满300返100"}); cbxType.SelectedIndex = 0; } }
可扩展性:如果增加“满500返200”的活动,只需要修改一下界面的combox和收费对象生成工厂中switch分支就可以实现扩展。
如果商场增加新的促销手段“满100积分10点”,以后积分到一定的程度可以兑换商品,也是很容易扩张的。
但是问题依然存在,简单工厂模式虽然也能解决这个问题,但这个模式只是解决对象的创建问题,而且由于工厂本身包括了所有的收费方式,商场是可能经常的更改打折额度和返利额度的,每次维护或者扩展都要改动这个工厂,以致代码需要重新便宜部署,这真是个不好的处理方法。
策略模式
策略模式是对算法的包装,是把使用算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法包装到一系列的策略类里面,作为一个抽象策略类的子类。用一句话来说,就是:"准备一组算法,并将每一个算法封装起来,使得它们可以互换。"
下面是一个示意性的策略模式结构图:
这个模式涉及到三个角色:
- 环境(Context)角色:持有一个Strategy类的引用。
- 抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。
对比一下简单工厂的模式结构图,Simple Factory模式角色与结构:
工厂类角色Creator (LightSimpleFactory):工厂类在客户端的直接控制下(Create方法)创建产品对象。
抽象产品角色Product (Light):定义简单工厂创建的对象的父类或它们共同拥有的接口。可以是一个类、抽象类或接口。
具体产品角色ConcreteProduct (BulbLight, TubeLight):定义工厂具体加工出的对象。
在学习策略模式时,学员常问的一个问题是:为什么不能从策略模式中看出哪一个具体策略适用于哪一种情况呢?
答案非常简单,策略模式并不负责做这个决定。换言之,应当由客户端自己决定在什么情况下使用什么具体策略角色。策略模式仅仅封装算法,提供新算法插入到已有系统中,以及老算法从系统中"退休"的方便,策略模式并不决定在何时使用何种算法。
策略模式定义了算法家族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。
结构示意性代码:
// "Strategy"abstract class Strategy{ // Methods abstract public void AlgorithmInterface();}// "ConcreteStrategyA"class ConcreteStrategyA : Strategy{ // Methods override public void AlgorithmInterface() { Console.WriteLine("Called ConcreteStrategyA.AlgorithmInterface()"); }}// "ConcreteStrategyB"class ConcreteStrategyB : Strategy{ // Methods override public void AlgorithmInterface() { Console.WriteLine("Called ConcreteStrategyB.AlgorithmInterface()"); }}// "ConcreteStrategyC"class ConcreteStrategyC : Strategy{ // Methods override public void AlgorithmInterface() { Console.WriteLine("Called ConcreteStrategyC.AlgorithmInterface()"); }}// "Context"class Context{ // Fields Strategy strategy; // Constructors public Context( Strategy strategy ) { this.strategy = strategy; } // Methods public void ContextInterface() { strategy.AlgorithmInterface(); }}/// <summary>/// Client test/// </summary>public class Client{ public static void Main( string[] args ) { // Three contexts following different strategies Context c = new Context( new ConcreteStrategyA() ); c.ContextInterface(); Context d = new Context( new ConcreteStrategyB() ); d.ContextInterface(); Context e = new Context( new ConcreteStrategyC() ); e.ContextInterface(); }}
策略模式实现商场收银系统:
CashContext类实现:
class CashContext { private CashSuper cs; public CashContext(CashSuper cs) { this.cs = cs; } public double GetResult(double money) { return cs.AcceptCash(money); } }
但是这样仅用策略模式的话,客户端就得负责根据不同的方案选择算法,又回到了原来的老路子了。所以这时候要把简单工厂模式和策略模式结合起来用,让CashContext这个类负责收费方式算法的生成。
改造后的CashContext类的实现:
class CashContext { private CashSuper cs; public CashContext(string type) { switch(type) { case "正常收费": cs = new CashNormal(); break; case "满300返100": cs = new CashReturn("300","100"); break; case "打8折": cs = new CashRebate("0.8"); break; default : break; } } public double GetResult(double money) { return cs.AcceptCash(money); } }
简单工厂只是负责造一个这样的对象,然后把对象的引用返回出去,具体怎么用,还是要在客户端中。
策略模式是给我一个对象的引用,我在里面帮你使用,然后给你一个统一的函数接口,你不容管自己调用的是什么类的引用。
简单工厂和策略模式结合后:负责制造一个对象,但是这时的对象不用传递出去了,直接在里面调用,然后开放一个统一的函数接口,客户端看不到任何与计算算法有关的类。
策略模式+简单工厂后的客户端代码:
public partial class Form1 : Form { double totalCash = 0; public Form1() { InitializeComponent(); } private void btnOK_Click(object sender, EventArgs e) { double sumPrice = 0; //计算一下该中商品的总价值 sumPrice = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtQuantity.Text); //利用工厂生产价格计算类,并调用相应的算法 CashContext cc = new CashContext(cbxType.SelectedItem.ToString()); //更新sumPrice的值,打折或者返现时的值 sumPrice = cc.GetResult(sumPrice); totalCash += sumPrice; //listbox显示每件商品的价钱 lbxList.Items.Add("单价: " + txtPrice.Text + " 数量: " + txtQuantity.Text + " "+ cbxType.SelectedItem + " 合计: " + sumPrice.ToString()); //lblResult中显示总价钱 lblResult.Text = totalCash.ToString(); } private void btnReset_Click(object sender, EventArgs e) { totalCash = 0; txtPrice.Text = ""; txtQuantity.Text = ""; lblResult.Text = totalCash.ToString(); //清除ListBox的内容 lbxList.Items.Clear(); cbxType.SelectedIndex = 0; } private void Form1_Load(object sender, EventArgs e) { cbxType.Items.AddRange(new object[] {"正常收费", "打8折", "满300返100"}); cbxType.SelectedIndex = 0; } }
这里还是有问题的,因为CashContext中还是用到了switch,也就是说,如果我们增加一种算法,还是要更改CashContext的代码。
解决这个问题的办法就是利用反射。
策略模式