當前位置:
首頁 > 知識 > 按鈕條件邏輯配置化的可選技術方案

按鈕條件邏輯配置化的可選技術方案

(點擊

上方公眾號

,可快速關注)




來源:琴水玉 ,


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技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 ImportNew 的精彩文章:

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

TAG:ImportNew |