按钮逻辑配置化的技术方案调研

问题

详情页的一些按钮逻辑,很容易因为产品的策略变更而变化,或因为来了新业务而新增条件判断,或因为不同业务的差异性而有所不同。如果修改代码来实现,需要重新发布,灵活性差。 可采用配置化的方法来实现按钮逻辑,从而在需要修改的时候只要变更配置即可。

本文讨论了三种可选方案: 重量级的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

}


规则引擎方案

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

条件表达式

对于轻量级判断逻辑,采用条件表达匹配。

优点: 轻量级

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

思路

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

扫描二维码关注公众号,回复: 2990883 查看本文章
 ifMatchX-ReturnRx,  ifMatchY-ReturnRy, ifMatchZ-ReturnRz, Else-ReturnDefault.

ifMatchX-ReturnRx 可以抽象成对象 (field, op, value, 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: 按钮逻辑是单个条件实现

import com.alibaba.fastjson.JSON;
import java.util.Map;
import lombok.Data;

@Data
public class SingleCondition extends BaseCondition implements ICondition {

  private Boolean result;

  public SingleCondition() {
    super();
    this.result = false;
  }

  public SingleCondition(String field, CondOp condOp, Object value, boolean result) {
    super(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.test(valueMap);
  }

}

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

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() {
    conditions = new ArrayList<>();
    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: 按钮逻辑配置的抽象:

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

  private Boolean defaultResult;

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

  public ButtonCondition(List<ICondition> matches, Boolean defaultResult) {
    this.buttonConditions = 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("buttonConditions");
    List<ICondition> allConditions = new ArrayList<>();
    for (int i=0; i < conditions.size(); i++) {
      Map condition = (Map) conditions.get(i);
      if (condition.containsKey("field")) {
        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: buttonConditions) {
      if (cond.satisfiedBy(valueMap)) {
        return cond.getResult();
      }
    }
    return defaultResult;
  }
}

STEP6: 再来一单的按钮逻辑配置及测试

@Test
def "testConditions"() {
    expect:
    def singleCondJson = '{"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 = '{"buttonConditions": [{"field": "activity_type", "op":"eq", "value": 63, "result": false}, {"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
}


按钮逻辑的修改

  • 新增

针对某个按钮新增逻辑,只要修改按钮逻辑配置即可。 这里需要注意, 新增按钮逻辑的配置可能需要新的字段,比如原来只要判断 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. 需要按照不同行业、不同业务定制化的按钮逻辑,可以考虑规则引擎。

猜你喜欢

转载自www.cnblogs.com/lovesqcc/p/9568899.html
今日推荐