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

前情提要

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

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

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

活动策划

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

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

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

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

解决之道

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

“我真是个天才, 哈哈哈哈”, 小光想着都快笑出声了…

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

1
2
3
4
public interface ActivityStrategy {
String getActivityPrice();
}

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

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
29
30
31
32
33
34
35
36
37
38
// 感恩节活动算法
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 "没有活动";
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 收银台
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());
}
}

投入使用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
}
}

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

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

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

故事之后

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

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

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

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

扩展阅读一

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

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

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

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

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

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

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
29
30
31
32
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为例, 其图片的磁盘缓存就使用了策略模式, 并提供了很多策略供用户选择:

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

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