在订单详情页中,经常有一些业务逻辑,根据不一样的条件展现不一样的文案。一般的写法是一堆嵌套的 if-else 语句,难以理解和维护。好比待开奖:html
if (Objects.equals(PAID, orderState)) { if (Objects.equals(LOTTERY, activity) { Map<String, Object> extra = orderBO.getExtra(); if (extra == null || extra.get("LOTTERY") == null) { return "待开奖"; } } } if (Objects.equals(LOTTERY, activity) && Objects.equals(CONFIRM, orderState) && isGrouped(orderBO.getExtra())) { return "待开奖"; } return OrderState.getState(orderState);
如何可以更好地表达这些业务呢 ?java
在 "业务逻辑配置化的可选技术方案" 一文中,讨论了“Groovy脚本”、“规则引擎”及“条件表达式”三种方案。 本文主要谈谈条件表达式方案的实现。
express
通过初步分析可知,问题域涉及:编程
这里,使用简单表达式来表示规则。 这样,解决域能够创建为: 表达式 - 实例 Map ,表达式为: 条件 - 结果json
这里的主要问题是:安全
仔细分析代码可知, 这些均可以凝练成 if cond then result 模式。 而且 or 能够拆解为单纯的 and 。好比上述代码能够拆解为:框架
state = PAID, activity = LOTTERY , extra is null => "待开奖" state = PAID, activity = LOTTERY , extra.containsNot(LOTTERY) => “待开奖” state = CONFIRM , activity = LOTTERY, extra.EXT_STATUS = "prize" => “待开奖”
这样,咱们把问题的解决方案再一次化简:ide
支持以下操做符:函数
isnull / notnull : 是否为 null , 不为 null测试
eq ( = ): 等于,好比 state = PAID => 待发货;
neq ( != ): 不等于,好比 visible != 0 => 可见订单;
in (IN) : 包含于 ,好比 state in [TOPAY, PAID, TOSEND] => 未关闭订单;
contains / notcontains (HAS, NCT): 包含, 好比 extra contains BUYER_PHONE
取值: 从 Map 中获取。支持支持点分好比 extra.EXT_STATUS 。 还能够提供一些计算函数,基于这个值作进一步的计算。
有两种可选配置语法:
JSON 形式。 好比 {"conditions": [{"field": "activity", "op":"eq", "value":"LOTTERU"},{"field": "state", "op":"eq", "value":"CONFIRM"},{"field": "extra.EXT_STATUS", "op":"eq", "value":"prize"}] , "result":"待开奖"} , 这种形式比较正规,不过有点繁琐,容易由于配置的一点问题出错。
简易形式。 好比 activity= LOTTERY && state = CONFIRM && extra.EXT_STATUS = "prize" , 写起来顺手,这样须要一套DSL 语法和解析代码, 解析会比较复杂一点。
经讨论后,使用 JSON 编写表达式比较繁琐,所以考虑使用简易形式。在简易形式中,规定:
JSON 的语法配置:
class ExpressionJsonTest extends Specification { ExrepssionJsonParser expressionJsonParser = new ExrepssionJsonParser() @Test def "testOrderStateExpression"() { expect: SingleExpression singleExpression = expressionJsonParser.parseSingle(singleOrderStateExpression) singleExpression.getResult(["state":value]) == result where: singleOrderStateExpression | value | result '{"cond": {"field": "state", "op":"eq", "value":"PAID"}, "result":"待发货"}' | "PAID" | '待发货' } @Test def "testOrderStateCombinedExpression"() { expect: String combinedOrderStateExpress = ''' {"conditions": [{"field": "activity", "op":"eq", "value":"LOTTERY"},{"field": "state", "op":"eq", "value":"PAID"}, {"field": "extra", "op":"isnull"}], "result":"待开奖"} ''' CombinedExpression combinedExpression = expressionJsonParser.parseCombined(combinedOrderStateExpress.trim()) combinedExpression.getResult(["state":"PAID", "activity":"LOTTERY"]) == "待开奖" } @Test def "testOrderStateCombinedExpression2"() { expect: String combinedOrderStateExpress = ''' {"conditions": [{"field": "activity", "op":"eq", "value":"LOTTERY"},{"field": "state", "op":"eq", "value":"PAID"}, {"field": "extra", "op":"notcontains", "value":"LOTTERY"}], "result":"待开奖"} ''' CombinedExpression combinedExpression = expressionJsonParser.parseCombined(combinedOrderStateExpress.trim()) combinedExpression.getResult(["state":"PAID", "activity":"LOTTERY", "extra":[:]]) == "待开奖" } @Test def "testOrderStateCombinedExpression3"() { expect: String combinedOrderStateExpress = ''' {"conditions": [{"field": "activity", "op":"eq", "value":"LOTTERY"},{"field": "state", "op":"eq", "value":"CONFIRM"}, {"field": "extra.EXT_STATUS", "op":"eq", "value":"prize"}], "result":"待开奖"} ''' CombinedExpression combinedExpression = expressionJsonParser.parseCombined(combinedOrderStateExpress.trim()) combinedExpression.getResult(["state":"CONFIRM", "activity":"LOTTERY", "extra":['EXT_STATUS':'prize']]) == "待开奖" } @Test def "testWholeExpressions"() { expect: String wholeExpressionStr = ''' [{"cond": {"field": "state", "op":"eq", "value":"PAID"}, "result":"待发货"}, {"conditions": [{"field": "activity", "op":"eq", "value":"LOTTERY"},{"field": "state", "op":"eq", "value":"CONFIRM"}], "result":"待开奖"}] ''' WholeExpressions wholeExpressions = expressionJsonParser.parseWhole(wholeExpressionStr) wholeExpressions.getResult(["state":"PAID"]) == "待发货" wholeExpressions.getResult(["state":"CONFIRM", "activity":"LOTTERY"]) == "待开奖" } }
简易语法的测试用例:
class ExpressionSimpleTest extends Specification { ExpressionSimpleParser expressionSimpleParser = new ExpressionSimpleParser() @Test def "testOrderStateExpression"() { expect: SingleExpression singleExpression = expressionSimpleParser.parseSingle(singleOrderStateExpression) singleExpression.getResult(["state":value]) == result where: singleOrderStateExpression | value | result 'state = PAID => 待发货' | "PAID" | '待发货' } @Test def "testOrderStateCombinedExpression"() { expect: String combinedOrderStateExpress = ''' activity = LOTTERY && state = PAID && extra isnull => 待开奖 ''' CombinedExpression combinedExpression = expressionSimpleParser.parseCombined(combinedOrderStateExpress.trim()) combinedExpression.getResult(["state":"PAID", "activity":"LOTTERY"]) == "待开奖" } @Test def "testOrderStateCombinedExpression2"() { expect: String combinedOrderStateExpress = ''' activity = LOTTERY && state = PAID && extra NCT LOTTERY => 待开奖 ''' CombinedExpression combinedExpression = expressionSimpleParser.parseCombined(combinedOrderStateExpress.trim()) combinedExpression.getResult(["state":"PAID", "activity":"LOTTERY", "extra":[:]]) == "待开奖" } @Test def "testOrderStateCombinedExpression3"() { expect: String combinedOrderStateExpress = ''' activity = LOTTERY && state = CONFIRM && extra.EXT_STATUS = "prize" => 待开奖 ''' CombinedExpression combinedExpression = expressionSimpleParser.parseCombined(combinedOrderStateExpress.trim()) combinedExpression.getResult(["state":"CONFIRM", "activity":"LOTTERY", "extra":['EXT_STATUS':'prize']]) == "待开奖" } @Test def "testWholeExpressions"() { expect: String wholeExpressionStr = ''' activity = LOTTERY && state = PAID && extra NCT LOTTERY => 待开奖 ; state = PAID => 待发货 ; activity = LOTTERY && state = CONFIRM && extra.EXT_STATUS = "prize" => 待开奖 ; ''' WholeExpressions wholeExpressions = expressionSimpleParser.parseWhole(wholeExpressionStr) wholeExpressions.getResult(["state":"PAID"]) == "待发货" wholeExpressions.getResult(["state":"PAID", "activity":"LOTTERY"]) == "待开奖" wholeExpressions.getResult(["state":"CONFIRM", "activity":"LOTTERY", "extra":['EXT_STATUS':'prize']]) == "待开奖" } }
STEP1: 定义条件测试接口 Condition 及表达式接口 Expression
public interface Condition { /** * 传入的 valueMap 是否知足条件对象 * @param valueMap 值对象 * 若 valueMap 知足条件对象,返回 true , 不然返回 false . */ boolean satisfiedBy(Map<String, Object> valueMap); } public interface Expression { /** * 获取知足条件时要返回的值 */ String getResult(Map<String, Object> valueMap); }
STEP2: 条件的实现
@Data public class BaseCondition implements Condition { private String field; private Op op; private Object value; public BaseCondition() {} public BaseCondition(String field, Op op, Object value) { this.field = field; this.op = op; this.value = value; } public boolean satisfiedBy(Map<String, Object> valueMap) { try { if (valueMap == null || valueMap.size() == 0) { return false; } Object passedValue = MapUtil.readVal(valueMap, field); switch (this.getOp()) { case isnull: return passedValue == null; case notnull: return passedValue != null; case eq: return Objects.equals(value, passedValue); case neq: return !Objects.equals(value, passedValue); case in: if (value == null || !(value instanceof Collection)) { return false; } return ((Collection)value).contains(passedValue); case contains: if (passedValue == null || !(passedValue instanceof Map)) { return false; } return ((Map)passedValue).containsKey(value); case notcontains: if (passedValue == null || !(passedValue instanceof Map)) { return true; } return !((Map)passedValue).containsKey(value); default: return false; } } catch (Exception ex) { return false; } } } @Data public class CombinedCondition implements Condition { private List<BaseCondition> conditions; public CombinedCondition() { this.conditions = new ArrayList<>(); } public CombinedCondition(List<BaseCondition> conditions) { this.conditions = conditions; } @Override public boolean satisfiedBy(Map<String, Object> valueMap) { if (CollectionUtils.isEmpty(conditions)) { return true; } for (BaseCondition condition: conditions) { if (!condition.satisfiedBy(valueMap)) { return false; } } return true; } } public enum Op { isnull("isnull"), notnull("notnull"), eq("="), neq("!="), in("IN"), contains("HAS"), notcontains("NCT"), ; String symbo; Op(String symbo) { this.symbo = symbo; } public String getSymbo() { return symbo; } public static Op get(String name) { for (Op op: Op.values()) { if (Objects.equals(op.symbo, name)) { return op; } } return null; } public static Set<String> getAllOps() { return Arrays.stream(Op.values()).map(Op::getSymbo).collect(Collectors.toSet()); } }
STEP3: 表达式的实现
@Data public class SingleExpression implements Expression { private BaseCondition cond; protected String result; public SingleExpression() {} public SingleExpression(BaseCondition cond, String result) { this.cond = cond; this.result = result; } public static SingleExpression getInstance(String configJson) { return JSON.parseObject(configJson, SingleExpression.class); } @Override public String getResult(Map<String, Object> valueMap) { return cond.satisfiedBy(valueMap) ? result : ""; } } public class CombinedExpression implements Expression { private CombinedCondition conditions; private String result; public CombinedExpression() {} public CombinedExpression(CombinedCondition conditions, String result) { this.conditions = conditions; this.result = result; } @Override public String getResult(Map<String, Object> valueMap) { return conditions.satisfiedBy(valueMap) ? result : ""; } public static CombinedExpression getInstance(String configJson) { try { JSONObject jsonObject = JSON.parseObject(configJson); String result = jsonObject.getString("result"); JSONArray condArray = jsonObject.getJSONArray("conditions"); List<BaseCondition> conditionList = new ArrayList<>(); if (condArray != null || condArray.size() >0) { conditionList = condArray.stream().map(cond -> JSONObject.toJavaObject((JSONObject)cond, BaseCondition.class)).collect(Collectors.toList()); } CombinedCondition combinedCondition = new CombinedCondition(conditionList); return new CombinedExpression(combinedCondition, result); } catch (Exception ex) { return null; } } } @Data public class WholeExpressions implements Expression { private List<Expression> expressions; public WholeExpressions() { this.expressions = new ArrayList<>(); } public WholeExpressions(List<Expression> expressions) { this.expressions = expressions; } public void addExpression(Expression expression) { this.expressions.add(expression); } public void addExpressions(List<Expression> expression) { this.expressions.addAll(expression); } public String getResult(Map<String,Object> valueMap) { for (Expression expression: expressions) { String result = expression.getResult(valueMap); if (StringUtils.isNotBlank(result)) { return result; } } return ""; } }
STEP4: 解析器的实现
public interface ExpressionParser { Expression parseSingle(String configJson); Expression parseCombined(String configJson); Expression parseWhole(String configJson); } /** * 解析 JSON 格式的表达式 * * SingleExpression: 单条件的一个表达式 * {"cond": {"field": "state", "op":"eq", "value":"PAID"}, "result":"待发货"} * * CombinedExpression: 多条件的一个表达式 * {"conditions": [{"field": "activity", "op":"eq", "value":"LOTTERY"},{"field": "state", "op":"eq", "value":"PAID"}, {"field": "extra", "op":"isnull"}], "result":"待开奖"} * * WholeExpression: 多个表达式的集合 * ''' * [{"cond": {"field": "state", "op":"eq", "value":"PAID"}, "result":"待发货"}, * {"conditions": [{"field": "activity", "op":"eq", "value":"LOTTERY"},{"field": "state", "op":"eq", "value":"CONFIRM"}], "result":"待开奖"}] * ''' * */ public class ExrepssionJsonParser implements ExpressionParser { @Override public Expression parseSingle(String configJson) { return JSON.parseObject(configJson, SingleExpression.class); } @Override public Expression parseCombined(String configJson) { try { JSONObject jsonObject = JSON.parseObject(configJson); String result = jsonObject.getString("result"); JSONArray condArray = jsonObject.getJSONArray("conditions"); List<BaseCondition> conditionList = new ArrayList<>(); if (condArray != null || condArray.size() >0) { conditionList = condArray.stream().map(cond -> JSONObject.toJavaObject((JSONObject)cond, BaseCondition.class)).collect(Collectors.toList()); } CombinedCondition combinedCondition = new CombinedCondition(conditionList); return new CombinedExpression(combinedCondition, result); } catch (Exception ex) { return null; } } @Override public Expression parseWhole(String configJson) { JSONArray jsonArray = JSON.parseArray(configJson); List<Expression> expressions = new ArrayList<>(); if (jsonArray != null && jsonArray.size() > 0) { expressions = jsonArray.stream().map(cond -> convertFrom((JSONObject)cond)).collect(Collectors.toList()); } return new WholeExpressions(expressions); } private static Expression convertFrom(JSONObject expressionObj) { if (expressionObj.containsKey("cond")) { return JSONObject.toJavaObject(expressionObj, SingleExpression.class); } if (expressionObj.containsKey("conditions")) { return CombinedExpression.getInstance(expressionObj.toJSONString()); } return null; } } /** * 解析简易格式格式的表达式 * * 条件与结果用 => 分开; 每一个表达式之间用 ; 区分。 * * SingleExpression: 单条件的一个表达式 * state = PAID => 待发货 * * CombinedExpression: 多条件的一个表达式 * activity = LOTTERY && state = PAID && extra = null => 待开奖 * * WholeExpression: 多个表达式的集合 * * state = PAID => 待发货 ; activity = LOTTERY && state = PAID => 待开奖 * * */ public class ExpressionSimpleParser implements ExpressionParser { // 条件与结果之间的分隔符 private static final String sep = "=>"; // 复合条件之间之间的分隔符 private static final String condSep = "&&"; // 多个表达式之间的分隔符 private static final String expSeq = ";"; // 引号表示字符串 private static final String quote = "\""; private static Pattern numberPattern = Pattern.compile("\\d+"); private static Pattern listPattern = Pattern.compile("\\[(.*,?)+\\]"); @Override public Expression parseSingle(String expStr) { check(expStr); String cond = expStr.split(sep)[0].trim(); String result = expStr.split(sep)[1].trim(); return new SingleExpression(parseCond(cond), result); } @Override public Expression parseCombined(String expStr) { check(expStr); String conds = expStr.split(sep)[0].trim(); String result = expStr.split(sep)[1].trim(); List<BaseCondition> conditions = Arrays.stream(conds.split(condSep)).filter(s -> StringUtils.isNotBlank(s)).map(this::parseCond).collect(Collectors.toList()); return new CombinedExpression(new CombinedCondition(conditions), result); } @Override public Expression parseWhole(String expStr) { check(expStr); List<Expression> expressionList = Arrays.stream(expStr.split(expSeq)).filter(s -> StringUtils.isNotBlank(s)).map(this::parseExp).collect(Collectors.toList()); return new WholeExpressions(expressionList); } private Expression parseExp(String expStr) { expStr = expStr.trim(); return expStr.contains(condSep) ? parseCombined(expStr) : parseSingle(expStr); } private BaseCondition parseCond(String condStr) { condStr = condStr.trim(); Set<String> allOps = Op.getAllOps(); Optional<String> opHolder = allOps.stream().filter(condStr::contains).findFirst(); if (!opHolder.isPresent()) { return null; } String op = opHolder.get(); String[] fv = condStr.split(op); String field = fv[0].trim(); String value = ""; if (fv.length > 1) { value = condStr.split(op)[1].trim(); } return new BaseCondition(field, Op.get(op), parseValue(value)); } private Object parseValue(String value) { if (value.contains(quote)) { return value.replaceAll(quote, ""); } if (numberPattern.matcher(value).matches()) { // 配置中一般不会用到长整型,所以这里直接转整型 return Integer.parseInt(value); } if (listPattern.matcher(value).matches()) { String[] valueList = value.replace("[", "").replace("]","").split(","); List<Object> finalResult = Arrays.stream(valueList).map(this::parseValue).collect(Collectors.toList()); return finalResult; } return value; } private void check(String expStr) { expStr = expStr.trim(); if (StringUtils.isBlank(expStr) || !expStr.contains(sep)) { throw new IllegalArgumentException("expStr must contains => "); } } }
STEP5: 配置集成
客户端使用,见 测试用例。 能够与 apollo 配置系统集成,也能够将条件表达式存放在 DB 中。
demo 完。
本文尝试使用轻量级表达式配置方案,来解决详情文案的多样化复合逻辑问题。 适用于 条件不太复杂而且相互独立的业务场景。
在实际编程实现的时候,不急于着手,而是先提炼出其中的共性和模型,并实现为简易框架,能够获得更好的解决方案。