什么是策略模式
定义一系列的算法,并将每一个算法单独进行封装,而且使它们可以相互替换,从而达到传递不同参数而执行不同算法的结果。
策略模式让算法独立于使用它的客户而独立变化
策略模式应用场景
策略模式的用意是针对一组算法或逻辑,将每一个算法或逻辑封装到具有共同接口的独立的类中,从而使得它们之间可以相互替换。策略模式使得算法或逻辑可以在不影响到客户端的情况下发生变化。说到策略模式就不得不提及OCP(Open Closed Principle) 开闭原则,即对扩展开放,对修改关闭。策略模式的出现很好地诠释了开闭原则,有效地减少了分支语句。
基本案例
案例模拟购物车场景,根据不同的会员级别执行不同的折扣。分为初级、中级、高级会员三种。
//策略模式 定义抽象方法 所有支持公共接口
abstract class Strategy {
// 算法方法
abstract void algorithmInterface();
}
class StrategyA extends Strategy {
@Override
void algorithmInterface() {
System.out.println("初级会员9.5折");
}
}
class StrategyB extends Strategy {
@Override
void algorithmInterface() {
System.out.println("中级会员8折");
}
}
class StrategyC extends Strategy {
@Override
void algorithmInterface() {
System.out.println("高级会员半价");
}
}
// 使用上下文维护算法策略
class Context {
Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void algorithmInterface() {
// 这里类似于代理模式,可以在方法的前后执行不同的逻辑
strategy.algorithmInterface();
}
}
class ClientTestStrategy {
public static void main(String[] args) {
Context context;
context = new Context(new StrategyA());
context.algorithmInterface();
context = new Context(new StrategyB());
context.algorithmInterface();
context = new Context(new StrategyC());
context.algorithmInterface();
}
}
提高篇
在实际开发中,常常会使用到case语句,根据不同的情况执行不同的逻辑。当情况较多时,大量的case、if语句会极大地降低代码可读性,后期维护也极为不便,因此在实际开发中应当避免大量case语句的使用,此时,就可以使用策略模式进行代码重构。
在我的一个项目中,需要根据前端传递的不同值,而对sql进行不同的拼接,最先使用的是case语句,并对case中具体的逻辑抽取了方法,代码如下。
/**
* 获取查询条件
*
* @param params 参数
* @param asTable 表别名
* @param joinTables 连表
* @return
* @throws ParseException
*/
private String getWhere(Map<String, Object> params, String asTable,
Map<String, Map<String, Object>> joinTables) throws ParseException {
List<String> expresses = Lists.newArrayList();
// 参数的key全部取出来封装成set集合
Set<String> cloneSet = new HashSet<>(params.keySet());
for (String key : cloneSet) {
// 遍历key
// 值
String value;
// 存值集合
List<String> values;
// 判断符号
switch (getOpt(key).toLowerCase()) {
case "in":
// 如果是in
// 将其转换成 list
values = paramsToList(params, key);
if (values.isEmpty()) {
// 处理完之后如果为空就不处理
break;
} else {
in(params, asTable, expresses, key, values);
}
break;
case "notin":
// 是notin,逻辑一样
// 将其转换成 list
values = paramsToList(params, key);
if (values.isEmpty()) {
break;
} else {
notIn(params, asTable, expresses, key, values);
}
break;
case "gt":
// 是大于号
gt(params, asTable, expresses, key);
break;
case "lt":
// 是小于号
lt(params, asTable, expresses, key);
break;
case "gte":
// 是大于等于
gte(params, asTable, expresses, key);
break;
case "lte":
// 是小于等于
lte(params, asTable, expresses, key);
break;
case "eq":
// 是等于
eq(params, asTable, expresses, key);
break;
case "like":
// 是like
like(params, asTable, expresses, key);
break;
case "btw":
// 是between
value = params.get(key).toString();
// 默认只允许用这三种连接符
if (!value.contains("~") ||
!value.contains("-") ||
!value.contains("/")) {
break;
}
between(params, asTable, expresses, key, value);
break;
case "null":
isNull(params, asTable, expresses, key);
break;
case "not":
// 不等于
notEquals(params, asTable, expresses, key);
break;
case "match":
match(params, asTable, expresses, key);
break;
case "join":
//join_表名对应的实体类_操作_字段
//join_name_in_ids
joinTable(params, joinTables, key);
default:
// 处理所有的key(如果key没有前缀,就都默认为等于)
defaultCase(asTable, expresses, key);
break;
}
}
return StringUtils.join(expresses, " and ");
}
上面代码极不方便阅读,后面case可能继续增加,从而使代码的可维护性逐渐降低。
优化思路:对于不同的case情况,每个case即为一个枚举,而case中具体的方法则为一个策略。通过枚举的值来决定new哪个策略类,从而执行不同的代码逻辑。
思路很明确,就是把每个case作为一个策略,根据不同的case而执行不同的代码。
先编写条件策略的抽象类,具体的逻辑为抽象方法,供策略类去继承
public abstract class BaseOptStrategy {
/**
* 处理option
* @param params 参数
* @param asTable 表别名
* @param expresses 拆分后的数据
* @param key #{param.key}
*/
public abstract void sqlOptHandler(Map<String, Object> params, String asTable, List<String> expresses, String key);
/**
* 参数转列名。取出符号后的参数,转下划线
*
* @param temp
* @return
*/
public static String getExpressColumn(String temp) {
return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, temp.split("_")[1]);
}
}
再编写不同的策略,继承BaseOptStrategy,这里只用lte和eq两个case作为举例。
EqStrategy
@Getter
public class EqStrategy extends BaseOptStrategy {
public EqStrategy() {
}
@Override
public void sqlOptHandler(Map<String, Object> params, String asTable, List<String> expresses, String key) {
String value = params.get(key).toString();
if (!StringUtils.isBlank(value)) {
expresses.add(asTable + "." + getExpressColumn(key) + " = #{params." + key + "}");
}
}
}
LteStrategy
@Getter
public class LteStrategy extends BaseOptStrategy {
public LteStrategy() {}
@Override
public void sqlOptHandler(Map<String, Object> params, String asTable, List<String> expresses, String key) {
String value = params.get(key).toString();
if (!StringUtils.isBlank(value)) {
expresses.add(asTable + "." + getExpressColumn(key) + " <= #{params." + key + "}");
}
}
}
再编写执行策略的代理类
public class StrategyProxy {
private BaseOptStrategy strategy;
public StrategyProxy(BaseOptStrategy strategy) {
this.strategy = strategy;
}
public void sqlOptHandler(Map<String, Object> params, String asTable, List<String> expresses, String key) {
strategy.sqlOptHandler(params, asTable, expresses, key);
}
}
这样,对于每个eq和lte都有了对应的策略类,调用sqlOptHandler之前,传入不同的BaseOptStrategy就可以执行不同的逻辑。
但是,如何判断具体应该传入哪种策略?这里就不使用case语句,而是使用枚举,根据枚举值去new不同的对象
策略枚举代码
@Getter
public enum OptEnums {
/**
* 通过key去new对象
*/
IN("in", new InStrategy()),
NOTIN("notin", new NotInStrategy()),
GT("gt", new GtStrategy()),
LT("lt", new LtStrategy()),
GTE("gte", new GteStrategy()),
LTE("lte", new LteStrategy()),
EQ("eq", new EqStrategy()),
BTW("btw", new BtwStrategy()),
LIKE("like", new LikeStrategy()),
NULL("null", new NullStrategy()),
NOT("not", new NeqStrategy()),
MATCH("match", new MatchStrategy()),
JOIN("join", new JoinStrategy()),
;
private String opt;
private BaseOptStrategy strategy;
OptEnums(String opt, BaseOptStrategy strategy) {
this.opt = opt;
this.strategy = strategy;
}
}
使用 OptEnums optEnums = Enum.valueOf(OptEnums.class, "枚举名称"); 就可以获取指定的枚举,优化后的getWhere代码如下
private String getWhere(Map<String, Object> params, String asTable,
Map<String, Map<String, Object>> joinTables) throws ParseException {
List<String> expresses = Lists.newArrayList();
// 参数的key全部取出来封装成set集合
Set<String> cloneSet = new HashSet<>(params.keySet());
for (String key : cloneSet) {
// 遍历key
Object value = params.get(key);
if (value != null && !"".equals(value)) {
String optName = getOpt(key).toUpperCase();
try {
OptEnums optEnums = Enum.valueOf(OptEnums.class, optName);
StrategyProxy factory = new StrategyProxy(optEnums.getStrategy());
factory.sqlOptHandler(params, asTable, expresses, key);
} catch (Exception e) {
String[] optAndExpressColumn = key.split("_");
if (optAndExpressColumn.length == 1) {
expresses.add(asTable + "." + CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, optAndExpressColumn[0])
+ "= #{params." + key + "}");
}
}
}
}
return StringUtils.join(expresses, " and ");
}
try中就是对不同的策略进行处理,代码简洁了很多。而default语句含义为,上面的case都不匹配的情况,而在枚举中,则对应为没有找到枚举的情况。在这里可以判断是否为空而决定执行case,也可以直接像上面代码一样try-catch。
事实上,上面代码依然可以优化,把catch中的代码作为默认执行策略,这里就不做过多的赘述。
策略模式是最符合OCP原则的设计模式,通过把不同的算法进行封装,程序去控制执行哪个代码,大大降低了代码的耦合性。