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

2018 年 9 月 10 日 ImportNew

(点击上方公众号,可快速关注)


来源:琴水玉 ,

www.cnblogs.com/lovesqcc/p/9568899.html


问题


详情页的一些按钮逻辑,很容易因为产品的策略变更而变化,或因为来了新业务而新增条件判断,或因为不同业务的差异性而有所不同。如果通过代码来实现,通常要写一串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. 需要按照不同行业、不同业务定制化的按钮逻辑,可以考虑规则引擎。


【关于投稿】


如果大家有原创好文投稿,请直接给公号发送留言。


① 留言格式:
【投稿】+《 文章标题》+ 文章链接

② 示例:
【投稿】《不要自称是程序员,我十多年的 IT 职场总结》:http://blog.jobbole.com/94148/

③ 最后请附上您的个人简介哈~



看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

登录查看更多
0

相关内容

【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
打怪升级!2020机器学习工程师技术路线图
专知会员服务
98+阅读 · 2020年6月3日
元学习与图神经网络逻辑推导,55页ppt
专知会员服务
128+阅读 · 2020年4月25日
机器学习相关资源(框架、库、软件)大列表
专知会员服务
39+阅读 · 2019年10月9日
微信小程序支持webP的WebAssembly方案
前端之巅
19+阅读 · 2019年8月14日
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
一个牛逼的 Python 调试工具
机器学习算法与Python学习
15+阅读 · 2019年4月30日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
C# 10分钟完成百度人脸识别
DotNet
3+阅读 · 2019年2月17日
Ceph的正确玩法之SSD作为HDD的缓存池
炼数成金订阅号
5+阅读 · 2019年2月14日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
这一次,彻底解决滚动穿透
IMWeb前端社区
35+阅读 · 2019年1月4日
Arxiv
5+阅读 · 2019年10月11日
VrR-VG: Refocusing Visually-Relevant Relationships
Arxiv
6+阅读 · 2019年8月26日
Neural Approaches to Conversational AI
Arxiv
8+阅读 · 2018年12月13日
Arxiv
6+阅读 · 2018年11月1日
q-Space Novelty Detection with Variational Autoencoders
Arxiv
7+阅读 · 2018年3月19日
Arxiv
3+阅读 · 2017年11月21日
VIP会员
相关资讯
微信小程序支持webP的WebAssembly方案
前端之巅
19+阅读 · 2019年8月14日
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
一个牛逼的 Python 调试工具
机器学习算法与Python学习
15+阅读 · 2019年4月30日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
C# 10分钟完成百度人脸识别
DotNet
3+阅读 · 2019年2月17日
Ceph的正确玩法之SSD作为HDD的缓存池
炼数成金订阅号
5+阅读 · 2019年2月14日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
这一次,彻底解决滚动穿透
IMWeb前端社区
35+阅读 · 2019年1月4日
Top
微信扫码咨询专知VIP会员