02. 商场促销 - 策略模式(Strategy Pattern)

2.1 商场收银软件

  • 时间: 2月27日22点   地点: 大鸟房间   人物: 小菜, 大鸟

     “小菜, 给你出个作业, 做一个商场收银软件, 营业员根据客户所购买商品的单价和数量, 向客户收费.”

     “就这个? 没问题啊.” 小菜说, “有两个文本框来输入单间和数量, 一个确定按钮来算出每种商品的费用, 有个列表框来记录商品的清单, 一个标签来记录总计, 对, 还需要一个按钮来重新开始, 不就行了? !”



  • 商场收银系统 v1.0 关键代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
double total = 0.0d;
public void onClick(View view) {
double totalPrice = Double.parseDouble(price.getText().toString())*
Double.parseDouble(num.getText().toString());
total += totalPrice;
builder.append("单价: " + price.getText().toString() + "" +
"数量: " + num.getText().toString() + "" +
"合计: " + totalPrice + "\n");
result.setText(builder.toString());
}

     “大鸟,” 小菜叫道, “来看看, 这不就是你要的收银软件吗? 我不到半小时就搞定了.”
     “哈哈, 很快嘛,” 大鸟说这, 看了看小菜的代码. 接着说: “现在我要求商场对商品搞活动, 所有的商品打八折.”
     “那不就在totalPrices 后面车以一个0.8 吗”
     “小子, 难道商场活动一结束, 不打折了, 你还要再改一遍程序代码, 然后再用改后的程序把所有的机器全部安装一次吗? 再说, 还有可能因为周年庆, 打五折的情况, 你怎么办?”
     小菜不好意思到: “啊, 我是想的简单了点. 其实只要加一个下拉选择框就可以解决你说的问题.”
     “大鸟笑而不语”

2.2 增加打折

  • 商场收银系统 v1.1 关键代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public void onClick(View view) {
    double totalPrice = 0d;
    String cut = spinner.getPrompt().toString();
    switch (cut) {
    case "八折":
    totalPrice = Double.parseDouble(price.getText().toString())
    * Double.parseDouble(num.getText().toString()) * 0.8;
    break;
    case "七折":
    totalPrice = Double.parseDouble(price.getText().toString())
    * Double.parseDouble(num.getText().toString()) * 0.7;
    case "五折":
    totalPrice = Double.parseDouble(price.getText().toString())
    * Double.parseDouble(num.getText().toString()) * 0.5;
    default:
    totalPrice = Double.parseDouble(price.getText().toString())
    * Double.parseDouble(num.getText().toString());
    }
    total += totalPrice;
    builder.append("单价: " + price.getText().toString() + "" +
    "数量: " + num.getText().toString() + "" +
    "合计: " + totalPrice + "\n");
    result.setText(builder.toString());
    }

     “这下可以了吧, 只要我实现把商场可能的打折都做成下啦选择框的项, 要变化的可能性就小多了.” 小菜说道.



     “这比刚才灵活性上是好多了, 不过重复代码很多, 像 Double.parseDouble(), 这里你就写了 8 遍, 而且4个分支要执行语句除了打着多少一位几乎没什么不同, 应该考虑重构一下. 不过这还不是最主要的, 现在我的需求来了, 商场活动加大, 需要有满300返100的促销算法, 你说怎么办?”
     “满300返100, 那要是700就要返200了? 这个必须要写个函数吧?
     “小菜呀, 看来之前的白教你了, 这里面看不出什么名堂吗?”
     “哦! 我想起来了, 你的意思是简单工厂模式吧, 对的对的, 我可以先写一个父类, 在继承它实现多个打折和返利的子类, 利用多态, 完成这个代码.”
     “你打算写几个字类?”
     “根据需求呀, 比如八折, 七折, 五折, 满300送100, 满200送50 …. 要几个写几个.”
     “小菜又不动脑子了, 比如八折, 七折, 五折, 满300送80, 你难道再去加子类? 你不想想看, 这当中那些是相同的, 哪些是不同的?”

2.3 简单工厂实现

     “对的, 这里打折基本都是一样的, 只要有个初始化参数就可以了. 满几送几的, 需要两个参数才行, 明白, 现在看来不麻烦了.”
     “面向对象编程, 并不是类越多越好, 类的划分是为了封装, 但分类的基础是抽象, 具有相同属性和功能的对象的抽象集合才是类. 打一折和打九折只是形式不同, 抽象分离出来, 所有的打折算法都是一样的, 所以打折算法应该是一个类. 好了, 空话已说了太多, 写出来才是真懂.”
     大约1个小时后, 小菜交出了第三份作业.



  • 现金收费抽象类

    1
    2
    3
    abstract class CashSuper {
    public abstract double acceptCash(double money);
    }
  • 正常收费子类

    1
    2
    3
    4
    5
    6
    class CashNormal extends CashSuper {
    @Override
    public double acceptCash(double money) {
    return money;
    }
    }
  • 打折收费子类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class CashRebate extends CashSuper {
    private double moneyRebate = 1d;
    public CashRebate(String moneyRebate) {
    this.moneyRebate = Double.parseDouble(moneyRebate);
    }
    @Override
    public double acceptCash(double money) {
    return 0;
    }
    }
  • 返利收费子类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class CashReturn extends CashSuper {
    private double moneyCondition = 0.0d;
    private double moneyReturn = 0.0d;
    public CashReturn(String moneyCondition, String moneyReturn) {
    this.moneyCondition = Double.parseDouble(moneyCondition);
    this.moneyReturn = Double.parseDouble(moneyReturn);
    }
    @Override
    public double acceptCash(double money) {
    double result = money;
    if (money > moneyCondition) {
    result = money - Math.floor(money / moneyCondition) * moneyReturn;
    }
    return 0;
    }
    }
  • 客户端程序主要部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void onClick(View view) {
    CashSuper csuper = new CashFactory.createCashAccept(spinner.getPrompt());
    double totalPrices = 0d;
    totalPrices = csuper.acceptCash(Double.parseDouble(price.getText().toString())
    * Double.parseDouble(num.getText().toString()));
    total += totalPrices;
    builder.append("单价: " + price.getText().toString() + "" +
    "数量: " + num.getText().toString() + "" +
    "合计: " + totalPrice + "\n");
    result.setText(builder.toString());
    }

     “大鸟, 搞定, 这次无论你要怎么改, 我都可以简单处理就行了.” 小菜自信满满地说.
     “是吗, 我要是需要打五折和满500送200的促销活动, 如何办?”
     “只要在现金工厂中加两个条件, 在界面的下拉选项里加两项, 就Ok 了.”
     “现金工厂!? 你当时生产钞票啊. 是收费对象生成工厂才准确. 说的不错, 如果我现在需要增加一种商场促销手段, 满100送10点积分, 以后积分到一定时候可以领取奖品如何做?”
     “有了工厂, 何难? 加一个积分算法, 构造方法有两个参数: 条件和返点, 然他继承CashSuper, 再到现金工厂, 哦, 不对, 是收费对象生成工厂里增加满100积分10点的分之条件, 再到界面稍加改动, 就行了.”
     “恩, 不错. 你对简单工厂用的很熟练了嘛.” 大鸟接着说: “简单工厂模式虽然也能解决这个问题, 但这个模式只是解决对象的创建问题, 而且由于工厂本身包括了所有的收费方式, 商场是可能经常性的更改打折额度和返利额度, 每次维护或扩折收费方式都要改懂这个工厂, 以致代码需要重新编译部署,这真的是很糟糕的处理方式, 所有用它不是最好的办法. 面对算法的市场变动, 应该有更好的办法. 好好去研究一下其他的设计模式, 你会找到答案的.”
     小菜进入了沉思中…..

策略模式(Strategy Pattern)

  • 时间: 2月28日19点 地点: 大鸟房间 人物: 小菜, 大鸟

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

策略模式(Strategy Pattern): 他定义了算法家族, 分别封装起来, 让他们之间可以互相替换, 此模式让算法的变化, 不会影响到使用算法的客户.

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



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

    1
    2
    3
    public abstract class Strategy {
    public abstract void algorithmInterface();
    }
  • ConcreteStrategy, 封装了具体的算法或行为, 继承于 Strategy

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 具体算法A
    class ConcreteStrategyA extends Strategy {
    // 算法A 实现方法
    @Override
    public void algorithmInterface() {
    System.out.println("算法A实现");
    }
    }
    // 具体算法B
    class ConcreteStrategyB extends Strategy {
    // 算法 实现方法
    @Override
    public void algorithmInterface() {
    System.out.println("算法B实现");
    }
    }
    // 具体算法
    class ConcreteStrategyC extends Strategy {
    // 算法C 实现方法
    @Override
    public void algorithmInterface() {
    System.out.println("算法C实现");
    }
    }
  • Context, 用一个 ConcreteStrategy 来配置, 维护一个队 Strategy 对象的引用.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 上下文
    class Context {
    Strategy strategy;
    public Context(Strategy strategy) {
    this.strategy = strategy;
    }
    public void contextInteface() {
    strategy.algorithmInterface();
    }
    }
  • 客户端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static void main(String[] args){
    Context context;
    context = new Context(new ConcreteStrategyA());
    context.contextInteface();
    context = new Context(new ConcreteStrategyB());
    context.contextInteface();
    context = new Context(new ConcreteStrategyC());
    context.contextInteface();
    }

策略模式实现

     “我明白了,” 小菜说, “我昨天写的 CashSuper 就是策略抽象, 而正常收费 CashNormal, 打折收费 CashRebate 和 返利收费 CashReturn 就是三个具体的策略, 也就是策略模式中说的具体算法, 对吧?”
     “是的,来吧, 你模仿策略模式的基本代码, 改写一下你的程序.”
     “其实不麻烦, 原来写的 CashSuper, CashNormal, CashRebate 和 CashReturn 都不用改了, 只要加一个 CashContext 类, 并改写一下客户端就行了”

  • 商场收银系统 v1.2


  • CashContext 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class CashContext {
    private CashSuper cs; // 声明一个 CashSuper 对象
    public CashContext(CashSuper cs) { // 构造方法传入
    this.cs = cs;
    }
    public double getResult(double money) {
    return cs.acceptCash(money); // 根据不同的策略获得结果
    }
    }
  • 客户端主要代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public void onClick(View view) {
    double totalPrice = 0d;
    String cut = spinner.getPrompt().toString();
    CashContext cc = null;
    switch (cut) { // 根据下拉选择框, 传入对应的策略
    case "正常收费":
    cc = new CashContext(new CashNormal());
    break;
    case "满300减100":
    cc = new CashContext(new CashReturn("300", "100"));
    break;
    case "打八折":
    cc = new CashContext(new CashRebate("0.8"));
    break;
    }
    // 通过Context 对 getResult 方法的调用, 得到具体的收费结果.
    // 让具体算法与客户端进行了隔离.
    totalPrice = cc.getResult(Double.parseDouble(price.getText().toString())
    * Double.parseDouble(num.getText().toString()));
    total += totalPrice;
    builder.append("单价: " + price.getText().toString() + "" +
    "数量: " + num.getText().toString() + "" +
    "合计: " + totalPrice + "\n");
    result.setText(builder.toString());
    }

     “大鸟, 代码是模仿着写出来了. 但我感觉这样子做不又回到了原来的老套路了吗, 在客户端去判断用哪一个算法?”
     “是的, 但是你有没有什么好的办法, 把这个判断的过程从客户端程序转移走呢?”
     “转移? 不明白, 原来我用简单工厂是可以转移的, 现在这个样子如何做到?”
     “难道简单工厂就一定要是一个单独的类吗? 难道不可以与策略模式的 Context 结合?”
     “哦, 我明白你的意思了. 我试试看.”

2.6 策略与简单工厂结合

  • 改造后的 CashContext

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class CashContext {
    private CashSuper cs;
    public CashContext(CashSuper cs) {
    this.cs = cs;
    }
    // 注意这参数不是具体的收费策略对象
    // 而是一个字符串, 表示收费类型
    public CashContext(String type) {
    switch (type) {
    case "正常收费":
    cs = new CashNormal();
    break;
    case "满300减100":
    cs = new CashReturn("300", "100");
    break;
    case "打八折":
    cs = new CashRebate("0.8");
    break;
    }
    }
    public double getResult(double money) {
    return cs.acceptCash(money);
    }
    }
  • 客户端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 客户端船体程序 (主要部分)
    public void onClick(View view) {
    double totalPrice = 0d;
    String cut = spinner.getPrompt().toString();
    CashContext csuper = new CashContext(cut);
    totalPrice = csuper.getResult(Double.parseDouble(price.getText().toString())
    * Double.parseDouble(num.getText().toString()));
    total += totalPrice;
    builder.append("单价: " + price.getText().toString() + "" +
    "数量: " + num.getText().toString() + "" +
    "合计: " + totalPrice + "\n");
    result.setText(builder.toString());
    }

     “恩, 原来简单工厂模式并非只有建一个工厂类的做法, 还可以这样子做. 此是比刚才的模仿策略模式的写法要清楚多了, 客户端代码简单明了.”
     “那和你写的简单工厂的客户端代码比呢? 观察一下, 找出它们的不同之处.”

1
2
3
4
5
6
7
8
9
// 简单工厂模式的用法
CashSuper csuper = CashFactory.createCashAccept(spinner.getPrompt().toString);
... = csuper.getResult(...);
// 策略模式与简单工厂结合的用法
CashSuper csuper = new CashContext(spinner.getPrompt().toString);
... = csuper.getResult(...);

     “你的意思是说, 简单工厂模式我需要让客户端认识两个类, CashSuper 和 CashFactory, 而策略模式与简单工厂结合的用法, 客户端就只需要认识一个类 CashContext 就可以了. 耦合更加降低.”
     “说的没错, 我们在客户端实例化的是 CashContext 的对象, 调用的是 CashContext 的方法 getResult, 这使得具体的收费算法彻底的与客户端分离. 连算法的父类 CashSuper 都不让客户端认识了.”

2.7 策略模式解析

     “回过头来反思一下策略模式, 策略模式是一种定义的一系列算法的方法, 从概念上来看, 所有这些算法完成的都是相同的工作, 只是显现不同, 他可以以相同的方式调用所有的算法, 减少了各种算法类与使用算法类之间的耦合[DPE]. 大鸟总结道.”
     “策略模式还有什么优点? 小菜问道.”
     “策略模式中的 Strategy 类层次为 Context 定义了一系列的可供重用的算法或行为. 继承有助于析取出这些算法中的公共功能[DP]. 对于打折, 返利或者其他的算法, 其实都是对实际商品收费的一种计算方式, 通过继承, 可以得到他们的公共功能, 你说这公共功能是什么?”
     “公共的功能就是获得计算费用的结果 getResult, 这使得算法间有了抽象的父类 CashSuper.”
     “对, 很好. 另外一个策略模式的优点是优化了单元测试, 因为每个算法都有自己的类, 可以通过自己的接口单独测试[DPE]
     “每个算法可保证他没有错误, 修改其中任一个时也不会影响其他的算法. 这真的是非常好.”
     “哈, 小菜今天表现不错, 我所想到的你都想到了.” 大鸟表扬小菜, “还有, 在最开始编程时, 你不得不在客户端的代码中为了判断用哪一个算法计算而用了 switch 条件分支, 这也是正常的. 因为, 当不同的行为堆砌在一个类中时, 就很难避免使用条件语句来选择合适的行为. 将这些行为封装在一个个独立的Strategy 类中, 可以在使用这些行为的类种消除条件语句[DP]. 就商场收银系统的例子而言, 在客户端的代码中就跳出条件语句, 比秒了大量的判断. 这是非常重要的进展. 你能用一句话来概括这个优点吗?“ 大鸟总结后问道.
     “策略模式封装了变化.” 小菜快速而坚定地说.
     “说得非常好, 策略模式就是用来封装算法的, 但在事件中, 我们发现可以用它来封装几乎任何类型的规则, 只要在分析过程中听到需要在不同的时间应用不同的业务规则, 就可以考虑使用策略模式处理这种变化的可能性[DPE].”
     “但我觉得在基本的策略模式中, 选择所用具体实现的职责由客户端对象承担, 并转给策略模式的 Context 对象[DPE]. 这本身并没有解除客户端需要选择判断的压力, 而策略模式与简单工厂模式结合后, 选择具体实现的职责也可以由 Context 类承担, 这就最大化的减轻了客户端的职责.
     “是的, 这已经比期初的策略模式好用了, 不过, 他依然不够完美”
     “哦, 还有什么不足嘛?”
     “因为在 CashContext 里还是用到了 switch, 也就是说, 如果我们需要增加一种算法, 比如 ‘满200减50’ 你就必须要改 CashContext 中的switch 代码, 这总还是让人不爽啊.”
     “那你说怎么办, 有需求就得改啊, 任何需求的变更都是需要成本的.”
     “但是成本的高低还是有差异的. 高手和菜鸟的区别就是高手可以花同样的代价获得最大的收益, 或者说做同样的事情花最小的代价. 面对同样的需求, 当然是改动越小越好.”
     “你的意思是说, 还有更好的办法?”
     “当然, 这个办法就是用到了反射技术, 不是常有人讲, ‘反射反射, 程序员的快乐’, 不过今天就不讲了, 以后会再提他的.”

~感谢捧场,您的支持将鼓励我继续创作~