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