Business scenario
Recently, I received a small demand in the company, which needs to expand the existing trial user application rules. Our scenario is roughly as follows:
if (Overseas users) { return false; } if (Billing user) { return false; } if (Unpaid user && No longer service period) { return false } if (Transfer to introduce users || Paying user || Push user) { return true; }
According to the above conditions, we can conclude that:
-
Our main process is mainly based on the relationship between and or.
-
If there is a mismatch, in fact, our subsequent processes do not need to be executed, but need to have a short-circuit function.
-
For the current situation, if I change it on the original basis, I just need to pay a little attention to solve the problem that the demand is not very big, but the maintainability is very poor.
After weighing later, I decided to refactor this part.
Rule executor
To meet this demand, I first sorted out the general design of our rule executor, and then I designed a V1 version to share with you. If you also have such a case, you can share a message with me. The following part is mainly about the design and implementation process and code
Design rule of executor
Abstraction of rules and implementation of rules
//Business data @Data public class RuleDto { private String address; private int age; } //Rule abstraction public interface BaseRule { boolean execute(RuleDto dto); } //Rule Template 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; } } //Specific rules - Example 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; } } //Specific rules - Example 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; } } //Constant definition public class RuleConstant { public static final String MATCH_ADDRESS_START= "Beijing"; public static final String MATCH_NATIONALITY_START= "China"; }
Actuator construction
public class RuleService { private Map<Integer, List<BaseRule>> hashMap = new HashMap<>(); private static final int AND = 1; private static final int OR = 0; public static RuleService create() { return new 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 boolean execute(RuleDto dto) { for (Map.Entry<Integer, List<BaseRule>> item : hashMap.entrySet()) { List<BaseRule> ruleList = item.getValue(); switch (item.getKey()) { case AND: //If it is a , and , relationship, execute synchronously System.out.println("execute key = " + 1); if (!and(dto, ruleList)) { return false; } break; case OR: //If it is an , or , relationship, execute in parallel System.out.println("execute key = " + 0); if (!or(dto, ruleList)) { return false; } break; default: break; } } return true; } private boolean and(RuleDto dto, List<BaseRule> ruleList) { for (BaseRule rule : ruleList) { boolean execute = rule.execute(dto); if (!execute) { //And relationship matching fails once, and false is returned return false; } } //And , all relationships match successfully, and , true is returned return true; } private boolean or(RuleDto dto, List<BaseRule> ruleList) { for (BaseRule rule : ruleList) { boolean execute = rule.execute(dto); if (execute) { //or , returns , true if the relationship matches one return true; } } //or , false is returned if none of the relationships match return false; } }
Call of actuator
public class RuleServiceTest { @org.junit.Test public void execute() { //Rule executor //Advantages: it is relatively simple. Each rule can be independent. The rule, data and actuator are separated, and the caller is relatively regular //Disadvantages: data depends on the common transmission object dto //1. Define init rule AgeRule ageRule = new AgeRule(); NameRule nameRule = new NameRule(); NationalityRule nationalityRule = new NationalityRule(); AddressRule addressRule = new AddressRule(); SubjectRule subjectRule = new SubjectRule(); //2. Construct the required data # create # dto RuleDto dto = new RuleDto(); dto.setAge(5); dto.setName("Zhang San"); dto.setAddress("Beijing"); dto.setSubject("mathematics");; //3. Build and execute # rule # execute through chain call boolean ruleResult = RuleService .create() .and(Arrays.asList(nationalityRule, nameRule, addressRule)) .or(Arrays.asList(ageRule, subjectRule)) .execute(dto); System.out.println("this student rule execute result :" + ruleResult); } }
summary
Advantages and disadvantages of rule executors
advantage:
-
It is relatively simple. Each rule can be independent. The rule, data and actuator are separated, and the caller is relatively regular;
-
I define the convert method in the rule template class to convert parameters, which can provide expansion for the scene data required by specific rules.
Disadvantages:
-
The upper and lower rule s are data dependent. If it is unreasonable to directly modify the public transmission object dto, it is recommended to build the data in advance.