按钮条件逻辑配置化的可选技术方案

栏目: Groovy · 发布时间: 5年前

内容简介:详情页的一些按钮逻辑,很容易因为产品的策略变更而变化,或因为来了新业务而新增条件判断,或因为不同业务的差异性而有所不同。如果通过代码来实现,通常要写一串if-elseif-elseif-else语句,且后续修改扩展比较容易出错,需要重新发布,灵活性差。 可采用配置化的方法来实现按钮逻辑,从而在需要修改的时候只要变更配置即可。按钮逻辑的代码形式一般是:本文讨论了三种可选方案: 重量级的Groovy脚本方案、轻量级的规则引擎方案、超轻量级的条件匹配表达式方案,重点讲解了条件匹配表达式方案。这里的代码实现仅作为

详情页的一些按钮逻辑,很容易因为产品的策略变更而变化,或因为来了新业务而新增条件判断,或因为不同业务的差异性而有所不同。如果通过代码来实现,通常要写一串if-elseif-elseif-else语句,且后续修改扩展比较容易出错,需要重新发布,灵活性差。 可采用配置化的方法来实现按钮逻辑,从而在需要修改的时候只要变更配置即可。按钮逻辑的代码形式一般是:

public Boolean getIsAllowBuyAgain() {
  if (ConditionA) {
    return BoolA;
  }
  if (ConditionB) {
    return BoolB;
  }
 
  if (CondtionC && !CondtionD && (ConditionE not in [v1,v2])) {
    return BoolC;
  }
  return BoolD;
}

本文讨论了三种可选方案: 重量级的Groovy脚本方案、轻量级的规则引擎方案、超轻量级的条件匹配表达式方案,重点讲解了条件匹配表达式方案。

这里的代码实现仅作为demo, 实际需要考虑健壮性及更多因素。 按钮逻辑实现采用了“组合模式”,解析配置采用了“策略模式”和“工厂模式”。

使用Groovy缓存脚本

优点:非常灵活通用,重量级配置方案

不足:耗时可能比较多,简单script脚本第一次执行比较慢, script脚本缓存后执行比较快, 可以考虑预热; 复杂的代码不易于配置,简单逻辑是可以使用Groovy配置的。

package button

import com.alibaba.fastjson.JSON
import org.junit.Test
import shared.conf.GlobalConfig
import shared.script.ScriptExecutor
import spock.lang.Specification
import spock.lang.Unroll
import zzz.study.patterns.composite.button.*

class ButtonConfigTest extends Specification {

    ScriptExecutor scriptExecutor = new ScriptExecutor()
    GlobalConfig config = new GlobalConfig()

    def setup() {
        scriptExecutor.globalConfig = config
        scriptExecutor.init()
    }

    @Test
    def "testComplexConfigByGroovy"() {
        when:
        Domain domain = new Domain()
        domain.state = 20
        domain.orderNo = 'E0001'
        domain.orderType = 0

        then:
        testCond(domain)
    }

    void testCond(domain) {
        Binding binding = new Binding()
        binding.setVariable("domain", domain)
        def someButtonLogicFromApollo = 'domain.orderType == 10 && domain.state != null && domain.state != 20'
        println "domain = " + JSON.toJSONString(domain)

        (0..100).each {
            long start = System.currentTimeMillis()
            println "someButtonLogicFromApollo ? " +
                    scriptExecutor.exec(someButtonLogicFromApollo, binding)
            long end = System.currentTimeMillis()
            println "costs: " + (end - start) + " ms"
        }

    }
}

class Domain {

    /** 订单编号 */
    String orderNo

    /** 订单状态 */
    Integer state

    /** 订单类型 */
    Integer orderType

}
package shared.script;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import groovy.lang.Binding;
import groovy.lang.Script;

import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import shared.conf.GlobalConfig;

@Component("scriptExecutor")
public class ScriptExecutor {

  private static Logger logger = LoggerFactory.getLogger(ScriptExecutor.class);

  private LoadingCache<String, GenericObjectPool<Script>> scriptCache;

  @Resource
  private GlobalConfig globalConfig;

  @PostConstruct
  public void init() {
    scriptCache = CacheBuilder
        .newBuilder().build(new CacheLoader<String, GenericObjectPool<Script>>() {
          @Override
          public GenericObjectPool<Script> load(String script) {
            GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
            poolConfig.setMaxTotal(globalConfig.getCacheMaxTotal());
            poolConfig.setMaxWaitMillis(globalConfig.getMaxWaitMillis());
            return new GenericObjectPool<Script>(new ScriptPoolFactory(script), poolConfig);
          }
        });
    logger.info("success init scripts cache.");
  }

  public Object exec(String scriptPassed, Binding binding) {
    GenericObjectPool<Script> scriptPool = null;
    Script script = null;
    try {
      scriptPool = scriptCache.get(scriptPassed);
      script = scriptPool.borrowObject();
      script.setBinding(binding);
      Object value = script.run();
      script.setBinding(null);
      return value;
    } catch (Exception ex) {
      logger.error("exxec script error: " + ex.getMessage(), ex);
      return null;
    } finally {
      if (scriptPool != null && script != null) {
        scriptPool.returnObject(script);
      }
    }

  }

}

规则引擎方案

按钮条件逻辑和规则集合非常相似,可以考虑采用一款轻量级的规则引擎。通过配置平台来管理按钮逻辑规则。

可参阅 Java Drools5.1 规则流基础【示例】 。当然,这里若选择 Java Drools 显然“重”了,可选用一款轻量级的 Java 开源规则引擎作为起点。

条件表达式

对于轻量级判断逻辑,采用条件表达匹配。条件表达匹配,实质是规则引擎的超轻量级实现。

优点: 超轻量级

不足: 可能不够灵活应对各种复杂场景。

思路

分析按钮方法的逻辑,可以看出它遵循一个套路:

ifMatchX-ReturnRx,  ifMatchY-ReturnRy, ifMatchZ-ReturnRz, Else-ReturnDefault.

ifMatchX-ReturnRx 可以抽象成对象 (left:(field, op, value), right:result) ,其中 field 的值从传入的参数对象 valueMap 获取。 MatchX 既可能是原子条件,也可能是组合条件(与逻辑)。

原子条件的运算符主要包含 等于 eq, 不等于 neq , 包含 in , 大于 gt ,小于 lt , 大于或等于 gte, 小于或等于 lte 。

代码实现

STEP1: 定义条件测试接口 ICondition

public interface ICondition {

  /**
   * 传入的 valueMap 是否满足条件对象
   * @param valueMap 值对象
   * 若 valueMap 满足条件对象,返回 true , 否则返回 false .
   */
  boolean satisfiedBy(Map<String,Object> valueMap);

  /**
   * 获取满足条件时要返回的值
   */
  Boolean getResult();

}

STEP2: 基本条件的测试实现

import java.util.Collection;
import java.util.Map;
import java.util.Objects;

import lombok.Data;

@Data
public class BaseCondition {

  protected String field;
  protected CondOp op;
  protected Object value;

  public BaseCondition() {}

  public BaseCondition(String field, CondOp op, Object value) {
    this.field = field;
    this.op = op;
    this.value = value;
  }

  public boolean test(Map<String, Object> valueMap) {
    try {
      Object passedValue = valueMap.get(field);
      switch (this.getOp()) {
        case eq:
          return Objects.equals(value, passedValue);
        case neq:
          return !Objects.equals(value, passedValue);
        case lt:
          // 需要根据格式转换成相应的对象然后 compareTo
          return ((Comparable)passedValue).compareTo(value) < 0;
        case gt:
          return ((Comparable)passedValue).compareTo(value) > 0;
        case lte:
          return ((Comparable)passedValue).compareTo(value) <= 0;
        case gte:
          return ((Comparable)passedValue).compareTo(value) >= 0;
        case in:
          return ((Collection)value).contains(passedValue);
        default:
          return false;
      }
    } catch (Exception ex) {
      return false;
    }
  }
}

STEP3: 按钮逻辑是单个条件实现

package zzz.study.patterns.composite.button;

import com.alibaba.fastjson.JSON;

import java.util.Map;

import lombok.Data;

@Data
public class SingleCondition implements ICondition {

  private BaseCondition cond;
  private Boolean result;

  public SingleCondition() {
  }

  public SingleCondition(String field, CondOp condOp, Object value, boolean result) {
    this.cond = new BaseCondition(field, condOp, value);
    this.result = result;
  }

  public static SingleCondition getInstance(String configJson) {
    return JSON.parseObject(configJson, SingleCondition.class);
  }

  /**
   * 单条件测试
   * 这里仅做一个demo,实际需考虑健壮性和更多因素
   */
  @Override
  public boolean satisfiedBy(Map<String, Object> valueMap) {
    return this.cond.test(valueMap);
  }

}

STEP4: 按钮逻辑是组合条件,必须所有条件 conditions 都满足才算测试通过,返回 Result ; 否则交由下一个条件逻辑配置处理。

package zzz.study.patterns.composite.button;

import com.alibaba.fastjson.JSON;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import lombok.Data;

@Data
public class MultiCondition implements ICondition {

  private List<BaseCondition> conditions;
  private Boolean result;

  public MultiCondition() {
    this.conditions = new ArrayList<>();
    this.result = false;
  }

  public MultiCondition(List<BaseCondition> conditions, Boolean result) {
    this.conditions = conditions;
    this.result = result;
  }

  public static MultiCondition getInstance(String configJson) {
    return JSON.parseObject(configJson, MultiCondition.class);
  }

  @Override
  public boolean satisfiedBy(Map<String, Object> valueMap) {
    for (BaseCondition bc: conditions) {
      if (!bc.test(valueMap)) {
        return false;
      }
    }
    return true;
  }
}

STEP5: 按钮逻辑配置的抽象:

package zzz.study.patterns.composite.button;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import lombok.Data;

@Data
public class ButtonCondition {

  private List<ICondition> buttonRules;

  private Boolean defaultResult;

  public ButtonCondition() {
    this.buttonRules = new ArrayList<>();
    this.defaultResult = false;
  }

  public ButtonCondition(List<ICondition> matches, Boolean defaultResult) {
    this.buttonRules = matches;
    this.defaultResult = defaultResult;
  }

  public static ButtonCondition getInstance(String configJson) {
    Map<String, Object> configMap = JSON.parseObject(configJson);
    Boolean result = ((JSONObject) configMap).getBoolean("defaultResult");
    JSONArray conditions = ((JSONObject) configMap).getJSONArray("buttonRules");
    List<ICondition> allConditions = new ArrayList<>();
    for (int i=0; i < conditions.size(); i++) {
      Map condition = (Map) conditions.get(i);
      if (condition.containsKey("cond")) {
        allConditions.add(JSONObject.parseObject(condition.toString(), SingleCondition.class));
      }
      else if (condition.containsKey("conditions")){
        allConditions.add(JSONObject.parseObject(condition.toString(), MultiCondition.class));
      }
    }
    return new ButtonCondition(allConditions, result);
  }

  public boolean satisfiedBy(Map<String, Object> valueMap) {
    // 这里是一个责任链模式,为简单起见,采用了列表遍历
    for (ICondition cond: buttonRules) {
      if (cond.satisfiedBy(valueMap)) {
        return cond.getResult();
      }
    }
    return defaultResult;
  }
}

STEP6: 按钮逻辑配置及测试

@Test
    def "testConditions"() {
        expect:
        def singleCondJson = '{"cond":{"field": "activity_type", "op":"eq", "value": 13}, "result": true}'
        def singleButtonCondition = SingleCondition.getInstance(singleCondJson)
        def valueMap = ["activity_type": 13]
        singleButtonCondition.satisfiedBy(valueMap) == true
        singleButtonCondition.getResult() == true

        def multiCondJson = '{"conditions": [{"field": "activity_type", "op":"eq", "value": 13}, {"field": "feedback", "op":"gt", "value": 201}], "result": false}'
        def multiButtonCondition = MultiCondition.getInstance(multiCondJson)
        def valueMap2 = ["activity_type": 13, "feedback": 250]
        multiButtonCondition.satisfiedBy(valueMap2) == true
        multiButtonCondition.getResult() == false

        def buttonConfigJson = '{"buttonRules": [{"cond":{"field": "activity_type", "op":"eq", "value": 63}, "result": false}, {"cond":{"field": "order_type", "op":"eq", "value": 75}, "result": false}, ' +
                               '{"conditions": [{"field": "state", "op":"neq", "value": 10}, {"field": "order_type", "op":"eq", "value": 0}, {"field": "activity_type", "op":"neq", "value": 13}], "result": true}], "defaultResult": false}'
        def combinedCondition = ButtonCondition.getInstance(buttonConfigJson)
        def giftValueMap = ["activity_type": 63]
        def giftResult = combinedCondition.satisfiedBy(giftValueMap)
        assert giftResult == false

        def knowledgeValueMap = ["activity_type": 0, "order_type": 75]
        def knowledgeResult = combinedCondition.satisfiedBy(knowledgeValueMap)
        assert knowledgeResult == false

        def periodValueMap = ["state": 20, "order_type": 0, "activity_type": 0]
        def periodResult = combinedCondition.satisfiedBy(periodValueMap)
        assert periodResult == true

        def complexValueMap = ["state": 20, "order_type": 0, "activity_type": 13]
        def complexResult = combinedCondition.satisfiedBy(complexValueMap)
        assert complexResult == false
    }

    @Unroll
    @Test
    def "testBaseCondition"() {
        expect:
        new BaseCondition(field, op, value).test(valueMap) == result

        where:
        field      | op         | value      | valueMap          | result
        'feedback' | CondOp.eq  | 201        | ['feedback': 201] | true
        'feedback' | CondOp.in  | [201, 250] | ['feedback': 201] | true
        'feedback' | CondOp.gt  | 201        | ['feedback': 202] | true
        'feedback' | CondOp.gte | 201        | ['feedback': 202] | true
        'feedback' | CondOp.lt  | 201        | ['feedback': 250] | false
        'feedback' | CondOp.lte | 201        | ['feedback': 250] | false
    }

支持多种配置语法

以上支持了从JSON串解析按钮逻辑的条件配置。不过用JSON写逻辑表达式,还是有些不够自然,容易出错。如果能用更自然的表达语法就更好了,比如:activity_type=13 && state = 30 , result = true 。 这样需要支持多种配置语法。 可以使用策略模式和工厂模式。 凡是需要多种可替换实现的算法,通常都可以采用策略模式和工厂模式。

STEP1: 定义条件配置的解析策略接口:

package zzz.study.patterns.composite.button.strategy;

import zzz.study.patterns.composite.button.ButtonCondition;
import zzz.study.patterns.composite.button.MultiCondition;
import zzz.study.patterns.composite.button.SingleCondition;

public interface ConditionParserStrategy {

  SingleCondition parseSingle(String express);
  MultiCondition parseMulti(String express);
  ButtonCondition parse(String express);
}

STEP2: 实现从JSON的解析策略,实际上就是从 SingleCondition , MultiCondition, ButtionCondition 里抽出 getInstance 方法:

package zzz.study.patterns.composite.button.strategy;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import zzz.study.patterns.composite.button.ButtonCondition;
import zzz.study.patterns.composite.button.ICondition;
import zzz.study.patterns.composite.button.MultiCondition;
import zzz.study.patterns.composite.button.SingleCondition;

public class JSONStrategy implements ConditionParserStrategy {

  @Override
  public SingleCondition parseSingle(String condJson) {
    return JSON.parseObject(condJson, SingleCondition.class);
  }

  @Override
  public MultiCondition parseMulti(String condJson) {
    return JSON.parseObject(condJson, MultiCondition.class);
  }

  @Override
  public ButtonCondition parse(String condJson) {
    Map<String, Object> configMap = JSON.parseObject(condJson);
    Boolean result = ((JSONObject) configMap).getBoolean("defaultResult");
    JSONArray conditions = ((JSONObject) configMap).getJSONArray("buttonRules");
    List<ICondition> allConditions = new ArrayList<>();
    for (int i=0; i < conditions.size(); i++) {
      // ... see code above
    }
    return new ButtonCondition(allConditions, result);
  }
}

STEP3: 定义更自然语法的一种实现(暂时留空):

package zzz.study.patterns.composite.button.strategy;

import zzz.study.patterns.composite.button.ButtonCondition;
import zzz.study.patterns.composite.button.MultiCondition;
import zzz.study.patterns.composite.button.SingleCondition;

public class DomainStrategy implements ConditionParserStrategy {

  @Override
  public SingleCondition parseSingle(String domainStr) {
    return null;
  }

  @Override
  public MultiCondition parseMulti(String domainStr) {
    return null;
  }

  @Override
  public ButtonCondition parse(String domainStr) {
    return null;
  }
}

STEP4: 定义解析策略工厂

package zzz.study.patterns.composite.button.strategy;

public class ParserStrategyFactory {

  public ConditionParserStrategy getParser(String format) {
    if ("json".equals(format)) {
      return new JSONStrategy();
    }
    return new DomainStrategy();
  }

}

STEP5: 客户端使用,将之前的 XXXCondition.getInstance 方法换成如下:

ConditionParserStrategy parserStrategy = new ParserStrategyFactory().getParser("json")
def singleButtonCondition = parserStrategy.parseSingle(singleCondJson)
def multiButtonCondition = parserStrategy.parseMulti(multiCondJson)
def combinedCondition = parserStrategy.parse(buttonConfigJson)

实际应用中,策略类及工厂类都应该是单例Component。

按钮逻辑的修改

  • 新增

针对某个按钮新增逻辑,只要修改按钮逻辑配置即可。 这里需要注意, 新增按钮逻辑的配置可能需要新的字段,比如原来只要判断 order_type, 现在需要增加 activity_type ,这就要求传入的 valueMap 能够一次性把该传的东西都传进去,否则就要改代码了。 通常, valueMap 应该预先传入 (order_type, activity_type, buy_way, state, …)。

  • 修改

通常是是修改现有的运算符和值。比如原来的逻辑要求 order_type = 5 , 现在要改成 order_type = 5 or 10 , 这样原来的配置为 {“field”: “order_type”, “op”:”eq”, “value”: 5} 要改成 {“field”: “order_type”, “op”:”in”, “value”: [5,10]}

方案选用

个人建议:

  1. 非常简单的条件情形,比如不超过三个条件的按钮逻辑,适合用条件匹配表达式;
  2. 略微复杂的条件情形, 比如有好几个条件,适合用 groovy 脚本;
  3. 需要按照不同行业、不同业务定制化的按钮逻辑,可以考虑规则引擎。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Machine Learning

Machine Learning

Kevin Murphy / The MIT Press / 2012-9-18 / USD 90.00

Today's Web-enabled deluge of electronic data calls for automated methods of data analysis. Machine learning provides these, developing methods that can automatically detect patterns in data and then ......一起来看看 《Machine Learning》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具