详情页的一些按钮逻辑,很容易因为产品的策略变更而变化,或因为来了新业务而新增条件判断,或因为不同业务的差异性而有所不同。如果通过代码来实现,通常要写一串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, 实际需要考虑健壮性及更多因素。 按钮逻辑实现采用了“组合模式”,解析配置采用了“策略模式”和“工厂模式”。
优点:非常灵活通用,重量级配置方案
不足:耗时可能比较多,简单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]}
个人建议: