The business scenario

Recently, I received a small demand from the company to expand the existing application rules for trial users. Our scenario might look something like this:

if(overseas user) {return false;
}

if(Swiping user) {return false;
}

if(Unpaid customers && No longer in service) {return false
}

if(turn to introduce the user | | subscribers | | push users) {return true;
}
Copy the code

According to the above conditions, we can come to the conclusion that:

  1. Our main process is based onandororThe relationship between.
  2. If there is a mismatch, in fact, we do not need to execute the subsequent process, but we need to have oneA short circuitThe function.
  3. For the current situation, if I based on the original, as long as a little attention to solve the needs of the problem is not very big, but said the maintainability is very poor.

I decided to refactor this part after some further weighing.

Rule actuator

In view of this requirement, I first combed 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 your comments with me. The following part is mainly about the design and implementation process and code.

Design of rule actuators

Abstraction of rules and implementation of rules

// Service 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; }}// Concrete 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; }}// Concrete 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";
}
Copy the code

Actuator build

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(a) {
        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 the relationship is and, execute synchronously
                    System.out.println("execute key = " + 1);
                    if(! and(dto, ruleList)) {return false;
                    }
                    break;
                case OR:
                    // If the relationship is or, 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) {// Return false if the and relationship fails to match once
                return false; }}// Return true if all relationships match successfully
        return true;
    }

    private boolean or(RuleDto dto, List<BaseRule> ruleList) {
        for (BaseRule rule : ruleList) {
            boolean execute = rule.execute(dto);
            if (execute) {
                // Return true if the or relationship matches one
                return true; }}// Return false if none of the relationships match
        return false; }}Copy the code

Call to the actuator

public class RuleServiceTest {

    @org.junit.Test
    public void execute(a) {
        // Rule executor
        // Advantages: relatively simple, each rule can be independent, rules, data, executor can be separated, the caller is relatively neat
        // Disadvantages: Data depends on the common transport object DTO

        //1. Define the rule 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");
        dto.setAddress("Beijing");
        dto.setSubject("Mathematics");;

        //3. Build and execute rule Execute with chained calls
        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); }}Copy the code

conclusion

The advantages and disadvantages of rule actuators

  • Advantages:
    1. Relatively simple, each rule can be independent, rules, data, the executor is split out, the caller is relatively neat;
    2. I define the convert method in the Rule template class to convert the parameters so that I can extend the scenario data that a particular Rule needs.
  • Disadvantages: The upper and lower rules are data dependent. It is not reasonable to directly modify the common transport object DTO, so it is recommended to build the data in advance.