回馈顾客, 活动搞起 --- 策略模式

2,709 阅读7分钟

前情提要

上集讲到, 小光引入了饮料机(工厂方法模式)改进了光氏饮品的生产过程. 现在如果要新上什么饮品, 改变配方什么的, 都很简单了, 直接增加一个饮料机, 或是替换/拿掉一个饮料机就可以了. 表妹再也不抱怨了.

小光也找了些饮料厂商拿到了一些试喝的饮料新品. 心想, 正好临近感恩节, 圣诞节, 双十二啥的, 我可以拿这些饮料新品来做些活动啊, 感恩下新老顾客啊... 这些新品小光可是自己亲身试喝过的, 绝对好喝, 小光不做奸商, :)

所有示例源码已经上传到Github, 戳这里

活动策划

小光以其独特的码农生意人思维(我也不知道这是什么...), 很快想出了几条活动方案:

  • 即日起, 到感恩节(11/24)那天, 所有饮品6折优惠.
  • 双十二当天, 满12立减2元.
  • 12月20号到圣诞节(12/25), 买热干面+饮料套餐送大苹果.

小光想出这些活动方案后, 屁颠屁颠去拿给表妹看. 没曾想, 表妹一脸不愉快, 这么多方案, 还这么负责, 我怎么记得住...(单细胞的表妹).

怎么办了, 小光可不想放弃自己好不容易想出的这些方案, 而且活动方案肯定会因为是不同节日而有所改变嘛.

解决之道

很快, 小光就想到了解决办法, 他将三种活动方案的算法做好, 内置在收银台. 在不同的日子里选用不用的算法策略.

"我真是个天才, 哈哈哈哈", 小光想着都快笑出声了...

照例, 收银员无需关注是什么具体的算法, 故而抽象出一个父级接口:

public interface ActivityStrategy {

    String getActivityPrice();
}

每个方案对应一个算法策略:

// 感恩节活动算法
public class ThanksGivingDayStrategy implements ActivityStrategy {

    @Override
    public String getActivityPrice() {
        // 经过一系列算法
        return "(感恩节)所有饮品一律5折";
    }
}

// 双十二算法
public class DoubleTwelveDayStrategy implements ActivityStrategy {

    @Override
    public String getActivityPrice() {
        // 经过一系列算法
        return "(双十二)满12立减2元";
    }
}

// 圣诞节算法
public class ChristmasStrategy implements ActivityStrategy {

    @Override
    public String getActivityPrice() {
        // 经过一系列算法
        return "(圣诞节)买热干面+饮品套餐, 送大苹果一个";
    }
}

// 默认算法(注意这个, 稍后的扩展阅读会说下这个Default实现的意义)
public class DefaultActivityStrategy implements ActivityStrategy {
    @Override
    public String getActivityPrice() {
        // 什么都不做
        return "没有活动";
    }
}

支持各种活动策略算法的收银台:

// 收银台
public class Checkstand {

    private ActivityStrategy mActivityStrategy;

    public Checkstand() {
        mActivityStrategy = new DefaultActivityStrategy();
    }

    public Checkstand(ActivityStrategy activityStrategy) {
        this.mActivityStrategy = activityStrategy;
    }

    public void setActivityStrategy(ActivityStrategy activityStrategy) {
        this.mActivityStrategy = activityStrategy;
    }

    public void printBill() {
        System.out.println("本次账单活动:" + mActivityStrategy.getActivityPrice());
    }
}

投入使用

活动方案算法和收银台完工之后, 小光立马投入了使用:

public class XiaoGuang {

    public static void main(String[] args) {

        // 收银台, 默认
        Checkstand checkstand = new Checkstand();
        checkstand.printBill();

        // 感恩节期间
        checkstand.setActivityStrategy(new ThanksGivingDayStrategy());
        checkstand.printBill();

        // 双十二
        checkstand.setActivityStrategy(new DoubleTwelveDayStrategy());
        checkstand.printBill();

        // 圣诞节期间
        checkstand.setActivityStrategy(new ChristmasStrategy());
        checkstand.printBill();
    }
}

结果, 也正如小光预料的:

本次账单活动:没有活动
本次账单活动:(感恩节)所有饮品一律5折
本次账单活动:(双十二)满12立减2元
本次账单活动:(圣诞节)买热干面+饮品套餐, 送大苹果一个

活动一经推出, 顾客果然是比以前更多了...
大家还对小光新推出的那些试喝饮料赞不绝口, 都觉得味道不错, 还很着很有意思的名字...

故事之后

照例, 故事之后, 我们用UML类图来梳理下上述的关系(关注收银台与活动策略算法之间的关系):

大家可能已经看出端倪了, 没错, 这就是策略模式.

策略模式(Strategy Pattern):
定义一组算法, 并将每一个单独算法封装起来, 让它们可以相互替换.

策略模式让算法独立于使用它的客户而变化, 例如如果明年小光的双十二活动改变了, 只需单独修改这个DoubleTwelveDayStrategy即可, 客户类(收银台Checkstand)无需改变, 也无需关注每个算法的具体实现.

扩展阅读一

实际上策略模式也还是利用抽象, 封装, 继承, 多态的面向对象特性, 来达到封装变化, 解耦合的. 典型的开闭原则的实践.

另外, 眼尖的同学可能看到, 貌似这个类图似曾相识啊. 前面讲的简单工厂工厂方法中的类图与此极其相似:

比较上图三个模式的红框部分, 我们可以发现, 相当一致. 在此明确下三者的关系与区别:

  1. 首先简单工厂工厂方法创建型的模式, 而策略模式行为型的模式.
  2. 所谓创建型就是说用来生产对象的, 注重的生产(new)这个部分, 用创建型的模式来代替直接new一个实例, 更多是想将直接的实例依赖通过不同的方法转化接口依赖.
  3. 所谓行为型模式更多是描述一种行为, A使用B, 怎么使用的这个关系上.

实际上, 在上个工厂方法的故事中, 我们就已经使用到了策略模式.

表妹选择不同的饮料机来那饮料, 这个行为实际上就是一个策略模式的体现, 回顾下表妹的代码:

public class Cousins {

    private IBeverageMachine mBeverageMachine;

    private void setBeverageMachine(IBeverageMachine machine) {
        this.mBeverageMachine = machine;
    }

    private Drink takeDrink() {
        if (mBeverageMachine == null) throw new NullPointerException("Should set Beverage Machine firstly.");

        return mBeverageMachine.makeDrink();
    }

    public static void main(String[] args) {

        Cousins cousins = new Cousins();

        // for A
        cousins.setBeverageMachine(new OrangeJuiceMachine());
        Drink drink = cousins.takeDrink();
        System.out.println(drink);

        // for B
        cousins.setBeverageMachine(new CokeMachine());
        System.out.println(cousins.takeDrink());

        // for D
        cousins.setBeverageMachine(new MilkTeaMachine());
        System.out.println(cousins.takeDrink());
    }
}

和我们这个收银台(Checkstand)是一样一样的, 上例中的模式使用实际可以理解成是这样:

  • 蓝色部分是工厂方法模式的使用, 作用在于生产出不同的饮品.
  • 红色部分是策略模式的使用, 作用在于让表妹根据实际情况选择不同的饮料机.

所以说模式的运用, 往往不是简单而单一, 很多时候是很多模式合在一起的.

扩展阅读二

在展示本例策略模式的UML类图时, 我们将DefaultActivityStrategy类标记成红色了, 这是为什么呢?

是因为这里我们用的这个DefaultActivityStrategy实际上也是一种设计模式的体现. 这个模式不在GoF的23中设计模式内, 但是绝对是一个很常用, 很实用的模式 --- 空对象模式.

空对象模式(Null Object Pattern):
用一个空(什么都不做的)对象来代替NULL.

空对象模式是一个很简单的设计模式, 也可以看成是一种编码习惯. 它小但是作用大:

  • 使用空对象模式可以减少很多我们对于对象是否为空的判断. 例如本例中, 如果Checkstand的无参构造函数我们没有new一个空对象, 那么后续的对于Checkstand实例各种调用我们可能就需要判断其mActivityStrategy是否为空. 如果遗漏, 很有可能导致null pointer异常.
  • 另外对于一些可以链式调用的对象, 如果我们要每次都判断是否为空会很影响我们的链式调用.

空对象模式经常会用来作为策略模式算法族中的一个, 来提供空策略.

扩展阅读三

策略模式由于其优秀的对外扩展性和对内封装性, 在一些SDK或是优秀开源库中会经常用到. 还是以Glide为例, 其图片的磁盘缓存就使用了策略模式, 并提供了很多策略供用户选择:

优秀源码总是设计巧妙但又易懂不晦涩.

活动搞起了, 小光热干面欢迎大家常来光临啊, 喜欢您就收藏, 喜欢您就关注...