按钮条件逻辑配置化的可选技术方案
Posted lovesqcc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了按钮条件逻辑配置化的可选技术方案相关的知识,希望对你有一定的参考价值。
问题
详情页的一些按钮逻辑,很容易因为产品的策略变更而变化,或因为来了新业务而新增条件判断,或因为不同业务的差异性而有所不同。如果通过代码来实现,通常要写一串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
}
按钮逻辑的修改
- 新增
针对某个按钮新增逻辑,只要修改按钮逻辑配置即可。 这里需要注意, 新增按钮逻辑的配置可能需要新的字段,比如原来只要判断 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]}
方案选用
个人建议:
非常简单的条件情形,比如不超过三个条件的按钮逻辑,适合用条件匹配表达式;
略微复杂的条件情形, 比如有好几个条件,适合用 groovy 脚本;
需要按照不同行业、不同业务定制化的按钮逻辑,可以考虑规则引擎。
以上是关于按钮条件逻辑配置化的可选技术方案的主要内容,如果未能解决你的问题,请参考以下文章
where 条件中的可选参数 - 如何提高性能 - PL/SQL