策略模式

商场促销

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

功能描述:

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

把商品单价、数量、总价值显示到一个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的代码。

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

posted @ 2014-12-16 17:24  stemon  阅读(236)  评论(0编辑  收藏  举报