在訂單詳情頁中,經常有一些業務邏輯,根據不一樣的條件展現不一樣的文案。一般的寫法是一堆嵌套的 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 完。
本文嘗試使用輕量級表達式配置方案,來解決詳情文案的多樣化複合邏輯問題。 適用於 條件不太複雜而且相互獨立的業務場景。
在實際編程實現的時候,不急於着手,而是先提煉出其中的共性和模型,並實現爲簡易框架,能夠獲得更好的解決方案。