商场促销-策略模式-读《大话设计模式》笔记

读《大话设计模式》第2章,阅读笔记和感悟,建议购买原书阅读,文中作者即原书《大话设计模式》一书作者。

1 商场收银软件

本章对应一个题目“做一个商场收银软件,营业员根据客户所购买商品的单价和数量,向客户收费。”

界面可设计如下:

商场收银系统界面
商场收银系统界面

界面采用WPF编写,界面相关代码就不贴了,下面是点击确定计算代码:

// 计算总计
double total = 0.0d;

private void btnOK_Click(object sender, RoutedEventArgs e)
{
  double totalPrices = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text);	// 计算每个商品的合计

  total = total + totalPrices;															// 将每个商品合计计入总计

  lbxList.Items.Add($"单价:{txtPrice.Text} 数量:{txtNum.Text} 合计:{totalPrices}"); // 在列表框中显示信息

  lblResult.Text = total.ToString();
}

小菜:很简单啊,不到半小时就可以搞定。

大鸟:现在要求商场对商品搞活动,所有商品打八折。

小菜:那不就是在totalPrice后面乘以一个0.8吗?

大鸟:问题是:难道商场活动结束,不打折了,还要再改一遍程序代码,然后再用改后的程序去把所有机器全部安装一次吗?再说,还有可能因为周年庆,打五折的情况,是你,你怎么办?

小菜:我想得是简单了点,其实只要加个下拉框就可以解决你说的问题。

大鸟微笑不语。

2 增加打折

商场收银系统v1.1关键代码如下:

double total = 0.0d;

public MainWindow()
{
  InitializeComponent();
  cbxType.Items.Add("正常收费");
  cbxType.Items.Add("打八折");
  cbxType.Items.Add("打七折");
  cbxType.Items.Add("打五折");
  cbxType.SelectedIndex = 0;
}


private void btnOK_Click(object sender, RoutedEventArgs 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($"单价:{txtPrice.Text} 数量:{txtNum.Text} {cbxType.SelectedItem} 合计:{totalPrices}");

  lblResult.Text = total.ToString();
}

小菜:”这下可以了吧,只要我事先把商场可能的打折做成下拉选择框的项,要变化的可能性就小多了。”

加入折扣
加入折扣

“这比刚才灵活性上是好多了,不过重复代码很多,像Convert.ToDouble(),你这里就写了8遍,而且4个分支要执行的语句除了打折多少外几乎没什么不同,应该考虑重构一下。不过这不是最主要的,现在我的需求又来了,商场的活动加大,需要有满300返100的促销算法,你说怎么办?”

“满300返200,那要是700就要返200了?这个必须要写函数了吧?”

“小菜呀,看来之前教你的白教了,这里看不出什么名堂吗?”

“哦!我想起来了,你的意思是简单工厂模式,是吧?对的对的,我可以先写一个父类,再继承它实现多个打折和反利的子类,利用多态,完成这个代码。”

“你打算写几个子类?”

“根据需求呀,比如八折、七折、五折、满300送100、满200送50……要几个写几个。”

“小菜又不动脑子了,有必要这样吗?如果我现在要三折,我要满300送80,你难道再去加子类?你不想想看,这当中哪些是相同的,哪些是不同的?”

3 简单工厂实现

“对的,这里打折基本都是一样的,只要有个初始化参数就可以了。满几送几的,需要两个参数才行,明白,现在看来不麻烦了。”

面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。打一折和九折只是形式的不同,抽象分析出来,所有的打折算法都是一样的,所以打折算法应该是一个类。好了,空话已说了太多,写出来才是真的懂。”

代码结构图

代码结构图
代码结构图

现金收费抽象类

/// <summary>
/// 现金收费抽象类
/// </summary>
abstract class CashSuper
{
  /// <summary>
  /// 收取现金,参数为原价,返回为当前价
  /// </summary>
  /// <param name="money"></param>
  /// <returns></returns>
  public abstract double AcceptCash(double money);
}

正常收费子类

/// <summary>
/// 正常收费子类
/// </summary>
class CashNormal : CashSuper
{
  /// <summary>
  /// 正常收费,原价返回
  /// </summary>
  /// <param name="money"></param>
  /// <returns></returns>
  public override double AcceptCash(double money)
  {
    return money;
  }
}

打折收费子类

/// <summary>
/// 打折收费子类
/// </summary>
class CashRebate : CashSuper
{
  private double moneyRebate = 1d;

  /// <summary>
  /// 打折收费,初始化时,必需输入折扣率,如八折,就是0.8
  /// </summary>
  /// <param name="moneyRebate"></param>
  public CashRebate(string moneyRebate)
  {
    this.moneyRebate = double.Parse(moneyRebate);
  }
  public override double AcceptCash(double money)
  {
    return money * moneyRebate;
  }
}

返利收费子类

/// <summary>
/// 返利收费子类
/// </summary>
class CashReturn : CashSuper
{
  private double moneyCondition = 0.0d;
  private double moneyReturn = 0.0d;

  /// <summary>
  /// 返利收费,初始化时必须要输入返利条件和返利值,比如满300返100,则moneyCondition为300,moneyReturn为100
  /// </summary>
  /// <param name="moneyCondition"></param>
  /// <param name="moneyReturn"></param>
  public CashReturn(string moneyCondition, string moneyReturn)
  {
    this.moneyCondition = double.Parse(moneyCondition);
    this.moneyReturn = double.Parse(moneyReturn);
  }

  public override double AcceptCash(double money)
  {
    double result = money;

    // 若大于返利条件,则需要减去返利值
    if (money >= moneyCondition)
    {
      result = money - Math.Floor(money / moneyCondition) * moneyReturn;
    }

    return result;
  }
}

现金收费工厂类

/// <summary>
/// 现金收费工厂类
/// </summary>
class CashFactory
{
  public static CashSuper CreateCashAccept(string type)
  {
    CashSuper cs = null;

    switch(type)
    {
      case "正常收费":
        cs = new CashNormal();
        break;
      case "满300返100":
        CashReturn cr1 = new CashReturn("300", "100");
        cs = cr1;
        break;
      case "打八折":
        CashRebate cr2 = new CashRebate("0.8");
        cs = cr2;
        break;
    }

    return cs;
  }
}

客户端程序主要部分

double total = 0.0d;

private void btnOK_Click(object sender, RoutedEventArgs e)
{
  // 利用简单工厂模式根据下拉选择框,生成相应的对象
  CashSuper csuper = CashFactory.CreateCashAccept(cbxType.SelectedItem.ToString());
  double totalPrices = 0d;

  // 通过多态,可以得到收取费用的结果
  totalPrices = csuper.AcceptCash(Convert.ToDouble(txtPrice.Text)*Convert.ToDouble(txtNum.Text));
  total = total + totalPrices;

  lbxList.Items.Add($"单价:{txtPrice.Text} 数量:{txtNum.Text} {cbxType.SelectedItem} 合计:{totalPrices}");

  lblResult.Text = total.ToString();
}

“大鸟,搞定,这次无论你要怎么改,我都可以简单处理就行了。”小菜自信满满地说。

“是吗?我要是需要打五折和满500送200的促销活动,如何办?”

“只要在现金工厂当中加两个条件,在界面的下拉框里加两项,就OK了。”

“现金工厂?!你当量生成钞票呀。是收费对象生成工厂才准确。说得不错,如果我现在需要增加一种商场促销手段,满100积分10点,以后积分到一定时候可以领取奖品如何做?”

“有了工厂,何难?加一个积分算法,构造方法有两个参数:条件和返点,让它继承CashSuper,再到现金工厂,哦,不对,是收费对象生成工厂里增加满100积分10点的分支条件,再到界面稍加改动,就行了。”

“嗯,不错。你对简单工厂用得很熟练了嘛。”大鸟接着说:“简单工厂模式虽然也能解决这个问题,但这个模式只是解决对象的创建问题,而且由于工厂本身包括了所有的收费方式,商场是可能经常性地更改打折额度和返利额度,每次维护或扩展收费方式都要改动这个工厂,以致代码需要重新编译部署,这真的是很糟糕的处理方式,所有用它不是最好的办法。面对算法的进学变动,应该有更好的办法。好好去研究一下其他的设计械,你会找到答案的。”

小菜进入了沉思中……

4 策略模式

小菜次日来找大鸟,说:“我找到相关的设计模式了,应该是策略模式(Strategy)。策略模式定义了算法家族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化,不会影响到使用算法的客户。看来商场收银系统应该考虑用策略模式?”

“你问我?你说呢?”大鸟笑道,“商场收银时如何促销,用打折还是返利,其实都是一些算法,用工厂来生成算法对象,这没有错,但算法本身只是一种策略,最重要的是这些算法是随时都可能互相替换的,这就是变化点,而封装变化点是我们面向对象的一种很重要的思维方式。我们来看看策略模式的结构图和基本代码。”

策略模式(Strategy)结构图
策略模式(Strategy)结构图

Strategy类,定义所有支持的算法的公共接口

// 抽象算法类
abstract class Strategy
{
  // 算法方法
  public abstract void AlgorithmInterface();
}

ConcreteStrategy,封装了具体的算法或行为,继承于Strategy

// 具体算法A
class ConcreteStrategyA:Strategy
{
  // 算法A实现方法
  public override void AlgorithmInterface()
  {
    Console.WriteLine("算法A实现");
  }
}

// 具体算法B
class ConcreteStrategyB : Strategy
{
  // 算法B实现方法
  public override void AlgorithmInterface()
  {
    Console.WriteLine("算法B实现");
  }
}

// 具体算法C
class ConcreteStrategyC : Strategy
{
  // 算法C实现方法
  public override void AlgorithmInterface()
  {
    Console.WriteLine("算法C实现");
  }
}

Context,用一个ConcreteStratege来配置,维护一个对Strategy对象的引用。

// 上下文
class Context
{
  Strategy strategy;

  // 初始化时,传入具体的策略对象
  public Context(Strategy strategy)
  {
    this.strategy = strategy;
  }

  // 上下文接口,根据具体的策略对象,调用其算法的方法
  public void ContextInterface()
  {
    strategy.AlgorithmInterface();
  }
}

客户端代码

static void Main(string[] args)
{
  Context context;

  // 由于实例化不同的策略,所以最终在调用context.ContextInterface();时,所获得的结果就不尽相同

  context = new Context(new ConcreteStrategyA());
  context.ContextInterface();

  context = new Context(new ConcreteStrategyB());
  context.ContextInterface();

  context = new Context(new ConcreteStrategyC());
  context.ContextInterface();
}

5 策略模式实现

“我明白了,”小菜说,“我昨天写的CashSuper就是抽象策略,而正常收费CashNormal、打折收费CashRebate和返利收费CashRetrun就是三个具体策略,也就是策略模式中说的具体算法,对吧?”

“是的,来吧,你模仿策略模式的基本代码,改写一下你的程序。”

“其实不麻烦,原来写的CashSuper、CashNormal、CashRebate和CashReturn都不用更改了,只要加一个CashContext类,并改写一下客户端就行了。”

商场收银系统v1.2 代码结构图

商场收银系统v1.2 代码结构图
商场收银系统v1.2 代码结构图

CashContext类

class CashContext
{
  private CashSuper cs;

  // 通过构造方法,传入具体的收费策略
  public CashContext(CashSuper csuper)
  {
    this.cs = csuper;
  }

  // 根据收费策略的不同,获得计算结果
  public double GetResult(double money)
  {
    return cs.AcceptCash(money);
  }
}

客户端主要代码

// 用于总计
double total = 0.0d;

private void btnOK_Click(object sender, RoutedEventArgs e)
{
  CashContext cc = null;

  // 根据下拉选择框,将相应的策略对象作为参数传入CashContext的对象中
  switch (cbxType.SelectedItem.ToString())
  {
    case "正常收费":
      cc = new CashContext(new CashNormal());
      break;
    case "满300返100":
      cc = new CashContext(new CashReturn("300", "100"));
      break;
    case "打8折":
      cc = new CashContext(new CashRebate("0.8"));
      break;
  }

  double totalPrices = 0d;

  // 通过对Context的GetResult方法的调用,可以得到收取费用的结果,让具体算法与客户进行了隔离
  totalPrices = cc.GetResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
  total = total + totalPrices;
  lbxList.Items.Add($"单价:{txtPrice.Text} 数量:{txtNum.Text} {cbxType.SelectedItem} 合计:{totalPrices}");
  lblResult.Text = total.ToString();
}

“大鸟,代码是模仿着写出来了。但我感觉这样子做不又回到了原来的老路了吗?在客户端去判断用哪一个算法?”

“是的,但是你有没有什么好办法,把这个判断的过程从客户端程序转移走呢?”

“转移?不明白,原来我用简单工厂是可以转移的,现在这样子如何做到?”

“难道简单工厂就一定要是一个单独的类吗?难道不可以与策略模式的Context结合?”

“哦,我明白你的意思了。我试试看。”

6 策略与简单工厂结合

改造后的CashContext

class CashContext
{
  CashSuper cs = null;

  // 注意参数不是具体的收费策略对象,而是一个字符串,表示收费类型
  // 将实例化具体策略的过程由客户端转移到Context类中。简单工厂的应用
  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;
    }
  }

  public double GetResult(double money)
  {
    return cs.AcceptCash(money);
  }
}

客户端窗体程序的主要部分代码

double total = 0.0d;
private void btnOK_Click(object sender, RoutedEventArgs e)
{
  // 根据下拉选择框,将相应的算法类型字符串传入CashContext的对象中
  CashContext cc = new CashContext(cbxType.SelectedItem.ToString());

  double totalPrices = 0d;
  totalPrices = cc.GetResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
  total = total + totalPrices;
  lbxList.Items.Add($"单价:{txtPrice.Text} 数量:{txtNum.Text} {cbxType.SelectedItem} 合计:{totalPrices}");
  lblResult.Text = total.ToString();
}

“嗯,原来简单工厂模式并非只有建一个工厂类的做法,还可以这样子做。此时比刚才的模仿策略模式的写法要清楚多了,客户端代码简单明了。”

“那和你写的简单工厂的客户端代码比呢?观察一下,找出它们的不同之处。”

// 简单工厂模式的用法
CashSuper csuper = CashFactory.CreateCashAccept(cbxType.SelectedItem.ToString());
... = csuper.GetResult(...);
// 策略模式与简单工厂结合的用法
CashContext csuper = new CashContext(cbxType.SelectedItem.ToString());
... = csuper.GetSult(...);

“你的意思是说,简单工厂模式我需要让客户端认识两个类,CashSuper和CashFactory,而策略模式与简单工厂结合的用法,客户端就只需要认识一个类CashContext就可以了。耦合更加降低。”

“说的没错,我们在客户端实例化的是CashContext的对象,调用的是CashContext的方法GetResult,这使得具体的收费算法彻底地与客户端分离。连算法的父类CashSuper都不让客户端认识了。”

7 策略模式解析

“回过头来反思一下策略模式,策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。”大鸟总结道。

“策略模式还有什么优点?”小菜问道。

策略模式的Statege类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。对于打折、返利或者其他的算法,其实都是对实际商品收费的一种计算方式,通过继承,可以得到它们的公共功能,你说这公共功能指什么?”

“公共的功能就是获得计算费用的结果GetResult,这使得算法间有了抽象的父类CashSuper。”

“对,很好。别外一个策略模式的优点是简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。

“每个算法可保证它没有错误,修改其中任一个时也不会影响其他的算法。这真的是非常好。”

“哈,小菜今天表现不错,我所想的你都想到了。”大鸟表扬了小菜,“还有,在最开始编程时,你不得不在客户端的代码中为了判断用哪一个算法计算而用了switch条件分支,这也是正常的。因为,当不同的行为堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为。将这些行为封装在一个个独立的Strategy类中,可以在使用这些行为的类中消除条件语句。就商场收银系统的例子而言,在客户端的代码中就消除条件语句,避免了大量的判断。这是非常重要的进展。你能用一句话来概况这个优点吗?”大鸟总结后问道。

“策略模式封闭了变化。”小菜快速而坚定的说。

“说得非常好,策略模式就是用来封装算法的,但在实践中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。

“但我感觉在基本的策略模式中,选择所用具体实现的职责由客户端对象承担,并转给策略模式的Context对象。这本身并没有解除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体实现的职责也可以由Context来承担,这就最大化地减轻了客户端职责。”

“是的,这已经比起初的策略模式好用了,不过,它依然不够完美。”

“哦,还有什么不足吗?”

“因为在CashContext里还是用到了switch,也就是说,如果我们需要增加一种算法,比如’满200送50’,你就必须要更改CashContext中的switch代码,这总还是让人很不爽呀。”

“那你说怎么办,有需求就得改呀,任何需求的变更都是需要成本的。

“但是成本的高低还是有差异的。高手和菜鸟的区别就是高手可以花同样的代码获得最大的收益或者说做同样的事花最小的代价。面对同样的需求,当然是改动越小越好。”

“你的意思是说,还有更好的办法?”

“当然。这个办法就是用到了反射技术,不是常有人讲,’反射反射,程序员的快乐’,不过今天就不讲了,以后会再提它的。”

“反射真有这么神奇?”小菜疑惑地望向了远方。

(注:抽象工厂模式章节有对反射的讲解)

本文几乎照抄原书章节,看书加手打文字,加深知识点理解,建议阅读原书效果更佳。

原文出处:乐趣课堂

原文链接:https://mp.weixin.qq.com/s/pIR3gmc6HfqKQjuRrFsfGQ

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论