zoukankan      html  css  js  c++  java
  • 详情文案的轻量级表达式配置方案

    背景###

    在订单详情页中,常常有一些业务逻辑,根据不同的条件展示不同的文案。通常的写法是一堆嵌套的 if-else 语句,难以理解和维护。比如待开奖:

    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);
    

    如何能够更好地表达这些业务呢 ?

    "业务逻辑配置化的可选技术方案" 一文中,讨论了“Groovy脚本”、“规则引擎”及“条件表达式”三种方案。 本文主要谈谈条件表达式方案的实现。

    问题域分析###

    经过初步分析可知,问题域涉及:

    • 规则:条件与结果。结果主要是字符串和布尔值,而条件则多种多样,涉及到不同业务领域。因此,要着重解决如何表达复合条件的问题。
    • 实例匹配。 以什么样的形式将实例传入。 如果以对象传入,那么就需要反射机制来获取字段,反而容易出错,因此,可以将实例转换为 Map 之后传入规则集合。

    这里,使用简单表达式来表示规则。 这样,解决域可以建立为: 表达式 - 实例 Map ,表达式为: 条件 - 结果

    这里的主要问题是:

    • 配置化地表达复合条件。
    • 创建易于编写的语法,能够安全可靠地转化为表达式对象。

    设计方案###

    基本思路####

    仔细分析代码可知, 这些都可以凝练成 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" => “待开奖”
    

    这样,我们把问题的解决方案再一次化简:

    • 条件组合仅支持 and 表达式的组合,足够所需, 值仅支持 数字、字符串 和 列表。
    • 结果仅支持字符串和布尔。

    条件表达式####

    支持如下操作符:

    • 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 完。

    小结###

    本文尝试使用轻量级表达式配置方案,来解决详情文案的多样化复合逻辑问题。 适用于 条件不太复杂并且相互独立的业务场景。

    在实际编程实现的时候,不急于着手,而是先提炼出其中的共性和模型,并实现为简易框架,可以得到更好的解决方案。

  • 相关阅读:
    ROS配置C++14环境
    ubantu查看环境变量
    C++指向函数的指针
    ubantu删除文件(夹)
    ROS环境搭建
    vmware workstation pro 安装ubantu虚拟机
    Win7下删除Ubuntu启动项
    ubantu16.04
    ubantu卸载软件
    github之克隆
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/10801301.html
Copyright © 2011-2022 走看看