首页 > 代码库 > 策略模式

策略模式

商场促销

现在小菜被要求做一个商场促销软件,界面如下所示。

功能描述:

输入每件商品的单价和数量,点击确定计算该种类商品的总价值。

把商品单价、数量、总价值显示到一个ListBox中。

在最下面显示此次购物的总交费额。

点击重置,清除所有的数据。总计显示为0。

image

小菜上来就写,实现的代码如下:

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下拉列表框。

image

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);        }    }
对比一下CashFactory、CashContext、改进后的CashContext发现:

简单工厂只是负责造一个这样的对象,然后把对象的引用返回出去,具体怎么用,还是要在客户端中。

策略模式是给我一个对象的引用,我在里面帮你使用,然后给你一个统一的函数接口,你不容管自己调用的是什么类的引用。

简单工厂和策略模式结合后:负责制造一个对象,但是这时的对象不用传递出去了,直接在里面调用,然后开放一个统一的函数接口,客户端看不到任何与计算算法有关的类。

策略模式+简单工厂后的客户端代码:

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的代码。

解决这个问题的办法就是利用反射。

策略模式