工作如何繁忙,生活如何糟心,至少求知的这一刻是我的…
应用场景概述
针对用户不同场景、不同输入采取不同算法或行为时,如果试图在一个方法内实现所有算法或行为,可能使得该方法数百行乃至更多的代码,新增需求便再添加一个分支,洋洋洒洒又是数十行代码甚至更多,维护这个方法会逐渐变得困难。
实际上,用户不关心该场景下的算法、行为有多复杂,对于一个良好的代码,我们要求它具有较强的可阅读性。为了向用户屏蔽复杂性,在OOP中比较直观的方法是将不同的算法、行为封装成一个个对象,这也使得相关的算法或行为得到被复用的机会:
public int doSth(int type) {
if (type == 0) {
ActionA aa = new ActionA();
return aa.exec();
} else if (type == 1) {
ActionB ab = new ActionB();
return ab.exec();
} else {
ActionDefault ad = new ActionDefault();
return ad.exec();
}
}
上述代码已经暴露了问题,这些对象“各自为政”,没有统一的接口管理它们,协作者在维护代码时,并不会严格遵循我们的想法:一定为这些Action类提供#exec方法。他们可能提供的是#exectue方法、也可能是#doAction方法,这一定程度上破坏了代码的可读性。同时,更为重要的是,这里存在冗余代码,既然都是在调用类似对象的#exec方法,那为何不使用多态来实现呢?
public interface Action {
int exec();
}
public class ActionA implements Action {
public int exec() {
return 0;
}
}
public class ActionB implements Action {
public int exec() {
return 1;
}
}
public class ActionDefault implements Action {
public int exec() {
return 2;
}
}
public class Sth {
public int doSth(int type) {
Action act;
if (type == 0) {
act = new ActionA();
} else if (type == 1) {
act = new ActionB();
} else {
act = new ActionDefault();
}
return act.exec();
}
}
对于doSth方法而言,是用户根据其应用场景创建合适的对象并调用相关方法,上述代码实现可以说是策略模式的雏形,Action接口是抽象策略,各个Action的实现类也就是各个具体策略,它们封装了具体的算法或行为。
策略模式UML
如下图所示,策略模式由三种成员构成:
- Context: 上下文管理器,负责持有一个策略对象的引用,为客户端提供调用方法,也就是客户端创建所需的策略对象,传入Context,调用Context的方法间接调用具体策略对象的方法;
- Strategy: 抽象策略接口,定义公共接口,相关的具体策略都应实现它;
- StrategyA/StrategyB/…: 具体策略实现类,封装具体的算法或行为。
在上一小节中并没有引入Context,事实上,如果只想直接地调用具体策略对象的method方法,并不需要引入Context,使用上一小节末的实现即可。Context的引入,可以在callMethod方法中实现对具体策略对象的功能增强,比如简单的例子:记录策略的执行时间、提供前处理和后处理等等;也可以将各个策略类中获取程序上下文的重复代码交由Context处理。
具体实现
现实中有太多的例子可匹配策略模式,比如PM希望你对各类会员对商品的价格有不同的折扣,需求如下:
- 普通会员:不打折;
- 高级会员:95折;
- 超级会员:9折。
如果我们不使用策略模式实现,我们很容易想到:
public float getPrice(User user, float oriPrice) {
switch(user.type) {
case MEMBER.VIP: return oriPrice * 0.95;
case MEMBER.SUPER_VIP: return oriPrice * 0.9;
default: return oriPrice;
}
}
事实上,最早在需求并不复杂的情况,过早的优化反而是未来的噩梦,如此实现就好了。而PM显然不会善罢甘休,他要求不断地细分用户级别,针对不同会员打不同的折,
协作者维护代码时也不会难以理解,无非是扩充MEMBER枚举类和增加case,无需刻意重构。
但人心的贪欲是无尽的,一心一意为资本服务的PM提出:
- 普通会员如果曾续费过高级会员或超级会员,并且过期时长不超过30天,前台会诱导用户续费会员服务,后台计算价格时根据续费会员等级给该用户提供一张95折或9折优惠券。
此时,switch case在嵌套分支下已难堪大任,我们应当考虑有更细致的价格计算,最简单的就是if分支替换它,但在复杂分支下它的可读性不佳,因此我们不该考虑使用if,尝试使用策略模式实现:
public interface PriceStrategy {
float getPrice(User user, float oriPrice);
}
public class NormalPriceStrategy implements PriceStrategy {
public float getPrice(User user, float oriPrice) {
if (user.superVipExpire < 31) return oriPrice * 0.9;
else if(user.vipExpire < 31) return oriPrice * 0.95;
else return oriPrice;
}
}
public class VipPriceStrategy implements PriceStrategy {
public float getPrice(User user, float oriPrice) {
return oriPrice * 0.95;
}
}
public class SVipPriceStrategy implements PriceStrategy{
public float getPrice(User user, float oriPrice) {
return oriPrice * 0.9;
}
}
public class PriceContext {
// 采取JavaBean模式,可能引发NullPointerExpetion,但这属于用户编程错误,故无需给ps默认初始化
private PriceStrategy ps;
public PriceStrategy getStrategy() {
return ps;
}
public void setStrategy(PriceStrategy ps) {
this.ps = ps;
}
public float getPrice(User user, float oriPrice) {
return ps.getPrice(user, oriPrice);
}
}
public class CalPriceService {
public float calPrice(User user, float oriPrice) {
PriceContext pc = new PriceContext();
switch(user.type) {
case MEMBER.VIP: pc.setStrategy(new VipPriceStrategy()); break;
case MEMBER.SUPER_VIP: pc.setStrategy(new SVipStrategy()); break;
default: pc.setStrategy(new NormalStrategy()); break;
}
return pc.getPrice(user, oriPrice);
}
}
可以看到,这次实现的PriceContext并没有附加功能,只是调用了具体策略对象的#getPrice方法,在上文也提过,如果场景比较简单无需对具体策略对象进行功能加强,就不需要实现Context,直接创建相应策略对象并调用相关方法即可。
看到客户端(CalPriceService)的代码时,相信读者对策略模式的缺陷有些明了:编写客户端的协作者必须清楚地知道不同等级会员相应的策略,也就是在什么场景下需要什么策略。那有方法为客户端屏蔽这一层复杂性吗?答案是有的:
- 抽象策略类应提供#isMatch(objects args…):boolean方法,各个具体策略类实现它,用以判断相关参数是否可匹配该策略;
- 在Context中通过反射获取到所有的策略类,并对其遍历传入相关参数即可得到相应的匹配策略。
- 在客户端调用时,通过Context对象传入相关参数,即可自动实例化相关策略。
策略模式优缺点
优点
- 避免嵌套分支带来的维护困难、可读性不佳问题;
- 遵循开闭原则;
- 相关算法、行为可复用;
- 将策略与Context分离,在Context中可对具体策略对象进一步功能增强。
缺点
- 可能会创建大量的策略对象;
- 客户端协作者必须知道在什么场景应用什么策略。
完美匹配某种模式的场景不多,设计模式只是一种思想并不是标准,无需严格遵守,按照项目的场景实现、改造即可。同时过早的优化,反而会引起后期维护不便,只需要实现当前功能,等到遇到某种瓶颈时再考虑重构、优化的事。