按鈕條件邏輯配置化的可選技術方案
(點擊
上方公眾號
,可快速關注)
來源:琴水玉 ,
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]}
方案選用
個人建議:
非常簡單的條件情形,比如不超過三個條件的按鈕邏輯,適合用條件匹配表達式;
略微複雜的條件情形, 比如有好幾個條件,適合用 groovy 腳本;
需要按照不同行業、不同業務定製化的按鈕邏輯,可以考慮規則引擎。
【關於投稿】
如果大家有原創好文投稿,請直接給公號發送留言。
① 留言格式:
【投稿】+《 文章標題》+ 文章鏈接
② 示例:
【投稿】《不要自稱是程序員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/
③ 最後請附上您的個人簡介哈~
看完本文有收穫?請轉發分享給更多人
關注「ImportNew」,提升Java技能


※趣圖:有那麼難么?不很簡單嘛?
※機器學習模型,能分清川菜和湘菜嗎?
TAG:ImportNew |