今天,我要干掉 if ... else ...
業務背景
近日在公司領到一個小需求,需要對之前已有的試用用戶申請規則進行拓展。我們的場景大概如下所示:
- if (是否海外用戶) {
- return false;
- }
- if (刷單用戶) {
- return false;
- }
- if (未付費用戶 && 不再服務時段) {
- return false;
- }
- if (轉介紹用戶 || 付費用戶 || 內推用戶) {
- return true;
- } else {
- return false;
- }
按照上述的條件我們可以得出的結論是:
咱們的的主要流程主要是基于 and 或者 or 的關系。
如果有一個不匹配的話,其實咱們后續的流程是不用執行的,就是需要具備一個短路的功能。
對于目前的現狀來說,我如果在原有的基礎上來修改,只要稍微注意一下解決需求不是很大的問題,但是說后面可維護性非常差。
后面進過權衡過后,我還是決定將這個部分進行重構一下。
規則執行器
針對這個需求,我首先梳理了一下咱們規則執行器大概的設計, 我們首先需要對規則進行抽象, 然后定義規則模板,然后通過規則模板去自己實現具體的規則,最后對于規則中可能會存在共享對象的轉換,我們提前在模板方法中定義即可,后期如果需要的話,可以對 DSL 語言或者增加腳本語言解析器,以及反射 class 文件的方式來實現動態拓展。
最后我設計了一個 V1 版本和大家一起分享一下,如果大家也有這樣的 case 可以給我分享留言,下面部分主要是設計和實現的流程和 code .
規則執行器的設計
對于我規則的執行器的設計,我收到 <<策略模式>> 和 << 規約模式>> 的啟發。 在這個場景咱們首先想到的就是將規則的自然語言轉換為程序代碼。在 DDD 設計中,我們可以選擇 DSL 方式來處理 Rule 的一種方式;對于業務數據處理或者其他的復雜流程,我們可以通過 Rule 模板來進行自定義實現具體的 Rule 策略。
對于規則執行器的處理步驟如下:
- 首先需要構造業務數據如用戶基本,用戶狀態,以及一些業務數據;
- 然后通過當前的上下文,獲取具體規則列表,這里可以從規則工廠中獲取;
- 然后調用規則執行方法拿到結果。
- 在執行的過程,對鏈接關系的處理,常用的關系有 and or not 等
抽象規則和定義模板
首先需要定義 BaseRule 作為 Rule 的一個抽象,定義 execute 方法為執行方法。然后定義 AbstractRule
作為規則模板,作為一個方法的公共實現,提供拓展點 convert 、executeRule 可以用戶轉換自定義 RuleDto 數據結構。 AddressRule 和 NationalityRule分別做為兩個實現 Rule 的具體策略或者說是具體實現。
- // 業務數據
- @Data
- public class RuleDto {
- private String address;
- private int age;
- }
- // 規則抽象
- public interface BaseRule {
- boolean execute(RuleDto dto);
- }
- // 規則模板
- public abstract class AbstractRule implements BaseRule {
- protected <T> T convert(RuleDto dto) {
- return (T) dto;
- }
- @Override
- public boolean execute(RuleDto dto) {
- return executeRule(convert(dto));
- }
- protected <T> boolean executeRule(T t) {
- return true;
- }
- }
- // 具體規則- 例子1
- public class AddressRule extends AbstractRule {
- @Override
- public boolean execute(RuleDto dto) {
- System.out.println("AddressRule invoke!");
- if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
- return true;
- }
- return false;
- }
- }
- // 具體規則- 例子2
- public class NationalityRule extends AbstractRule {
- @Override
- protected <T> T convert(RuleDto dto) {
- NationalityRuleDto nationalityRuleDto = new NationalityRuleDto();
- if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
- nationalityRuleDto.setNationality(MATCH_NATIONALITY_START);
- }
- return (T) nationalityRuleDto;
- }
- @Override
- protected <T> boolean executeRule(T t) {
- System.out.println("NationalityRule invoke!");
- NationalityRuleDto nationalityRuleDto = (NationalityRuleDto) t;
- if (nationalityRuleDto.getNationality().startsWith(MATCH_NATIONALITY_START)) {
- return true;
- }
- return false;
- }
- }
- // 常量定義
- public class RuleConstant {
- public static final String MATCH_ADDRESS_START= "北京";
- public static final String MATCH_NATIONALITY_START= "中國";
- }
規則執行器的核心構建
RuleService 是規則執行和規則管道鏈接的具體類,在這個類里面我們首先提供了一個構造器方法 create()可以提供默認的初始化過程
- // 規則執行器
- public class RuleService {
- private Map<Integer, List<BaseRule>> hashMap = new HashMap<>();
- private static final int NOT = 2;
- private static final int AND = 1;
- private static final int OR = 0;
- private RuleDto ruleDto;
- public static RuleService create(RuleDto ruleDto) {
- RuleService ruleService = new RuleService();
- ruleService.ruleDto = ruleDto;
- return ruleService;
- }
- public RuleService and(List<BaseRule> ruleList) {
- hashMap.put(AND, ruleList);
- return this;
- }
- public RuleService or(List<BaseRule> ruleList) {
- hashMap.put(OR, ruleList);
- return this;
- }
- public RuleService not(List<BaseRule> ruleList) {
- hashMap.put(NOT, ruleList);
- return this;
- }
- public boolean execute() {
- return this.execute(ruleDto);
- }
- private boolean execute(RuleDto dto) {
- for (Map.Entry<Integer, List<BaseRule>> item : hashMap.entrySet()) {
- List<BaseRule> ruleList = item.getValue();
- switch (item.getKey()) {
- case AND:
- // 如果是 and 關系,同步執行
- System.out.println("execute key = " + 1);
- if (!andRule(dto, ruleList)) {
- return false;
- }
- break;
- case OR:
- // 如果是 or 關系,并行執行
- System.out.println("execute key = " + 0);
- if (!orRule(dto, ruleList)) {
- return false;
- }
- break;
- case NOT:
- // 如果是 not 關系
- System.out.println("execute key = " + 2);
- if (!notRule(dto, ruleList)) {
- return false;
- }
- default:
- break;
- }
- }
- return true;
- }
- private boolean andRule(RuleDto dto, List<BaseRule> ruleList) {
- for (BaseRule rule : ruleList) {
- boolean execute = rule.execute(dto);
- if (!execute) {
- // and 關系匹配失敗一次,返回 false
- return false;
- }
- }
- // and 關系全部匹配成功,返回 true
- return true;
- }
- private boolean orRule(RuleDto dto, List<BaseRule> ruleList) {
- for (BaseRule rule : ruleList) {
- boolean execute = rule.execute(dto);
- if (execute) {
- // or 關系匹配到一個就返回 true
- return true;
- }
- }
- // or 關系一個都匹配不到就返回 false
- return false;
- }
- private boolean notRule(RuleDto dto, List<BaseRule> ruleList) {
- // not 規則內部為 and 鏈接
- return !andRule(dto, ruleList);
- }
- }
- // 規則工廠類
- public class RuleServices {
- /**
- * 學生規則教研
- *
- * @return
- */
- public static RuleService isValidStudent(RuleDto ruleDto) {
- AgeRule ageRule = new AgeRule();
- NameRule nameRule = new NameRule();
- NationalityRule nationalityRule = new NationalityRule();
- AddressRule addressRule = new AddressRule();
- SubjectRule subjectRule = new SubjectRule();
- Flag110Rule flag110Rule = new Flag110Rule();
- return RuleService
- .create(ruleDto)
- .and(Arrays.asList(nationalityRule, nameRule, addressRule))
- .or(Arrays.asList(ageRule, subjectRule))
- .not(Collections.singletonList(flag110Rule));
- }
- }
客戶端調用代碼
客戶端調用主要分為三個步驟:
首先是需要構造業務數據,因為規則策略,是基于數據處理的。
然后從規則工廠中,獲取規則列表后返回規則定義執行器。
最后執行規則,返回結果。
- public class RuleServiceTest {
- @org.junit.Test
- public void execute() {
- //規則執行器
- //優點:比較簡單,每個規則可以獨立,將規則,數據,執行器拆分出來,調用方比較規整
- //缺點:數據依賴公共傳輸對象 dto
- //1. 構造需要的數據 create dto
- RuleDto dto = new RuleDto();
- dto.setAge(5);
- dto.setName("張三");
- dto.setAddress("北京");
- dto.setSubject("數學");;
- //2. 定義規則 init rule
- RuleService ruleService = RuleServices.isValidStudent(dto);
- //3. 規則執行 rule execute
- boolean ruleResult = ruleService.execute();
- System.out.println("this student rule execute result :" + ruleResult);
- }
- }
總結
規則執行器的優點和缺點
優點:
- 比較簡單,每個規則可以獨立,將規則,數據,執行器拆分出來,調用方比較規整;
- 我在 Rule 模板類中定義 convert 方法做參數的轉換這樣可以能夠,為特定 rule 需要的場景數據提供拓展。
缺點:上下游 rule 有數據依賴性,如果直接修改 dto 傳輸對象的值不是特別合理,這種建議采用中間數據存儲臨時數據。
參考資料
https://www.codenong.com/30430818
https://cloud.tencent.com/developer/article/1528935