原文出处:
琴水玉
问题
详情页的一些按钮逻辑,很容易因为产品的策略变更而变化,或因为来了新业务而新增条件判断,或因为不同业务的差异性而有所不同。如果通过代码来实现,通常要写一串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 G