优雅的实现策略

Xiao Qiang Lv4

优雅的实现策略

阅读本文大约10min

什么是easy rule

  • easy rule 是一个rule engine工具,用来代替复杂的if else block,在代码中使得业务能够得到更好的分离,从而让代码能够更好的使用策略模式来分离业务
  • easy rule除了支持java以外还支持,GoGroovyC#
  • 更多关于rule engine的details,给大家推荐一篇老马的文章RulesEngine
  • 与之相同的还有很多其他的java language的rule engine

如何使用easy rule

  • 如何用不同的方式创建rule
    • 以面向对象的形式(OO)
      • 添加依赖,根据项目而定,mavengradle都支持,在本文中使用maven
        1
        2
        3
        4
        5
        <dependency>
        <groupId>org.jeasy</groupId>
        <artifactId>easy-rules-core</artifactId>
        <version>4.1.0</version>
        </dependency>
      • 创建抽象类BasicTestRule继承easyRule中的BasicRule, 重载evaluate() & execute()方法,前者为了执行不同的rule匹配规则,后者为了自定义执行满足条件之后的操作,🌰 :打印某些日志,或者改变原先的facts的值;在以下的例子中,将对传入的facts与规则匹配上的时候在重载的execute()方法中执行了setter
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        public abstract class BasicTestRule extends BasicRule {

        public BasicTestRule(final String name, final int priority) {
        super(name, DEFAULT_DESCRIPTION, priority);
        }

        protected abstract boolean hitCondition(final RuleResult creatureFacts);
        protected abstract String getType();

        @Override
        public final boolean evaluate(final Facts facts) {
        return hitCondition(facts.get(FACTS));
        }

        @Override
        public final void execute(final Facts facts) {
        RuleResult ruleResult = facts.get(FACTS);
        ruleResult.setResultType(String.format("The class is %s", getType()));
        }
        }
        • 在以上的例子中,evaluate函数是放置了一个Condition函数hitCondition(),一旦条件符合就会返回TRUE,表明该rule被匹配
      • 定义满足条件的抽象函数hitCondition,每个rule继承Basic方法的之后,实现hitCondition方法,以demo中的PersonTestRule中被重载的函数为例:
        1
        2
        3
        4
        @Override
        protected boolean hitCondition(RuleResult creatureFacts) {
        return Objects.equals(creatureFacts.getClassType(), this.getClass().getSimpleName());
        }
        • 当传入的 facts的type和当前类名一致时,将会被匹配上,并且在BasicTestRule中,同样重载了easyRule提供的execute(),当rule被匹配之后,将会执行该函数。在父类BasicTestRule中,提供了抽象方法getType(),在不同的子类型中会返回自己类的全称,然后被父类中execute()调用
          1
          2
          3
          4
          @Override
          protected String getType() {
          return PersonTestRule.class.toString();
          }
    • annotation的形式
      • 给类定义Rule名称,该注解将会把该类标记成一个rule,标记之后可以用上述步骤在Rules中注册

      @Rule(name = "AnnotationTestRule", description = "using annotation to match rule")

      • 定义一个Condition注解的方法,这里定义满足条件的标准
        1
        2
        3
        4
        @Condition
        public boolean hitCondition(@Fact("facts") RuleResult ruleResult) {
        return Objects.equals(ruleResult.getClassType(), this.getClass().getSimpleName());
        }
      • 定义一个Action注解的方法,这里定义当条件满足时的要做的业务
        1
        2
        3
        4
         @Action
        public void getType() {
        System.out.println(AnnotationTestRule.class);
        }
    • Expression的形式定义rule
      1
      2
      3
      4
      5
      Rule testRule = new MVELRule()
      .name("Test Rule")
      .description("if class Type is `test`")
      .when("ruleResult.classType == 'test'")
      .then("ruleResult.setResultType('test');");
      • 初始化MVELRule,定义rule的namedescription,触发条件when,action then
      • tips,定义Facts时需要保持key value和表达式中操作对象的名称一致
        • facts.put("ruleResult", ruleResult);
    • fluent programmatic的形式定义rule
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      Rule rule = new RuleBuilder()
      .description("test")
      .name("checkAdult")
      .when(condition -> {
      RuleResult facts = condition.get("facts");
      return facts.getClassType().equals("test");
      })
      .then(facts -> {
      RuleResult ruleResult = facts.get("facts");
      System.out.println("--------matches-------");
      ruleResult.setResultType("test");
      })
      .build();
      • 创建RuleBuilder,以builder的形式定义了Rule的描述description,名称name,条件when以及满足条件之后需要执行的行为then
    • rule yml的形式定义rule
      • 定义一个yml文件,同样的,MVELRuleFactory也支持json格式的文件
      • 加入依赖
        1
        2
        3
        4
        5
        <dependency>
        <groupId>org.jeasy</groupId>
        <artifactId>easy-rules-mvel</artifactId>
        <version>4.1.0</version>
        </dependency>
      • 使用fileReader读取文件,构造到MVELRuleFactory
        • 首先先定义一个MVELRule的Factory,入参是用来接受rule文件的解析格式,easyRule支持yml&json两种
          1
          2
          3
          4
          5
          private MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
          ```
          * 如果在一个yml文件中定义了多个rule,使用`createRules`,否则使用`createRule`
          ```java
          Rules multipleRules = ruleFactory.createRules(new InputStreamReader(resourceAsStream));
        • MVEL语法
        • 如果遇到以下的错误 [Error: could not access field: org.zqf.easyruledemo.Person.age]
          • 我的解决方法是将Person变成一个公有的方法 原因是我当时第一次写的时候,是将Person定义在了当前的测试类中,所以当前class的级别为default,使用的范围是在当前包下。但是当easy rule的要使用person类中的方法时,因为没有在同一个包下,所以访问不了。
      • 第一个🌰 是在一个yml文件中定义了多个规则,但是每一个规则都是一个简单规则,使用---分隔
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        ---
        name: non adult rule
        description: when age is less than or equal 18, then mark as non-adult
        priority: 1
        condition: "person.age <= 18"
        actions:
        - "person.setAdult(false);"
        ---
        name: adult rule
        description: when age is greater than 18, then mark as adult
        priority: 2
        condition: "person.age > 18"
        actions:
        - "person.setAdult(true);"
      • 支持组合规则,适用于较为复杂的场景
        • UnitRuleGroup,在yml中定义compositeRuleTypeUnitRuleGroup,被定义的规则需要都被匹配上才能被选择上,如果有一个子规则没有匹配上,那么结果仍然不会被匹配,这些规则具有原子性
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          name: adult check composing rule
          ompositeRuleType: UnitRuleGroup
          riority: 1
          omposingRules:
          name: adult
          description: If the age is more than 18
          priority: 1
          condition: "person.age > 18"
          actions:
          - "person.setAdult(true);"
          name: foo-adult
          description: If the name is 'foo'
          priority: 1
          condition: "person.name == 'foo'"
          actions:
          - "person.setAdult(true);"
        • ConditionalRuleGroup,举个🌰:这种group类似于一种组合拳,有一个规则必须先执行,然后再看有没有其他的规则能够匹配上,就像登录一样,验证通过之后,再去做一些其他的操作
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          name: adult check Condiftional rule Group
          compositeRuleType: ConditionalRuleGroup
          priority: 1
          composingRules:
          - name: adult
          description: If the age is more than 18
          priority: 1
          condition: "person.age > 18"
          actions:
          - "person.setAdult(true);"
          - name: foo-adult
          description: If the name is 'foo'
          priority: 2
          condition: "person.name == 'foo'"
          actions:
          - "person.setAdult(true);"

          • tips,ConditionalRuleGroup将会检查composingRules中的优先级,其中只允许有一个priority=1子规则
        • ActivationRuleGroup,只会有一个子规则匹配上,那么就会忽略其他的规则,直接执行这个被选择上的规则
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          name: adult check Condiftional rule Group
          compositeRuleType: ActivationRuleGroup
          priority: 1
          composingRules:
          - name: adult
          description: If the age is more than 18
          priority: 1
          condition: "person.age > 18"
          actions:
          - "person.setAdult(true);"
          - name: foo-adult
          description: If the name is 'foo'
          priority: 2
          condition: "person.name == 'foo'"
          actions:
          - "person.setAdult(true);"
    • 对于spring boot项目,可以使用注解的方式,注入Rules
      • 定义一个Rule的configuration class,对该类使用注解@Configuration,spring boot的bean容器就会在启动的时候,将当前父类BasicVegetableRule下所有子类都加载到入参basicVegetableRules中,当然如果想要使用java的bean的便利,那么对于需要加载的子类也需要加上注解@Component。这样在spring boot项目启动的时候就会将其扫描到这个Set中。Rule是一个interface,所以所有的实现都可以转成其本身。
        1
        2
        3
        4
        5
        6
        7
        @Configuration
        public class RuleBeans {
        @Bean("BasicVegetableRule")
        public Rules BasicVegetableRules(final Set<BasicVegetableRule> basicVegetableRules) {
        return new Rules(basicVegetableRules.stream().map(Rule.class::cast).collect(Collectors.toSet()));
        }
        }
        1
        2
        3
        4
        @Component
        public class PotatoRule extends BasicVegetableRule {
        xxxxx;
        }
      • 在spring boot项目中,有一些不同的业务场景,可能需要用到大量的if else来完成不同的业务场景。将这些Component利用Java bean加载到Set对象中,将其构造到Rules,进而可以直接传入ruleEngine中完成规则匹配
  • 如何使用ruleEngine
    • 定义RuleEngine,初始化rule parameter参数
      1
      2
      3
      4
      RulesEngineParameters parameters = new RulesEngineParameters()
      .skipOnFirstAppliedRule(true)
      .skipOnFirstFailedRule(false)
      .skipOnFirstNonTriggeredRule(false);
      • setSkipOnFirstAppliedRule,这个配置决定了是否匹配到第一个rule就直接返回了
      • isSkipOnFirstNonTriggeredRule,当第一个规则没有被匹配成功的时候,如果当前值设置为true的时候,将不会执行其他的规则,一般可以设置为false,我理解这个是给最高优先级的规则匹配,如果第一个匹配不上,不用关心其他的规则
      • skipOnFirstFailedRule,当第一个匹配到的规则失败的时候,将会返回匹配到的下一个规则
    • 创建ruleEngine DefaultRulesEngine rulesEngine = new DefaultRulesEngine(parameters);
    • 定义Rule,并注册rule
      1
      2
      Rules rules = new Rules();
      rules.register(new PersonTestRule());
    • 定义Facts
      1
      2
      3
      4
      Facts facts = new Facts();
      RuleResult ruleResult = new RuleResult();
      ruleResult.setClassType(classType);
      facts.put(FACTS, ruleResult);
    • 最后定义一个通用的方法,使用RuleEngine的fire方法,来触发规则匹配
      rulesEngine.fire(rules, facts);

优缺点

  • rule engine可以将复杂的业务解耦,使每个类的职责更单一,方便维护与后期的扩展
  • 对于较为简单的规则,因为rule的优先级不同,以及被分散在每一个类中,这样可能不如使用if else直观,需要有足够的上下文,才能将每个rule联系起来

Q&A

  • 如果两个rule的优先级相同,rule在Rules的顺序是什么呢?
    • answer:当两个rule的优先级相同时,匹配rule的顺序会使用rule的注册顺序,反之使用优先级(priority)排序

Demo

  • Title: 优雅的实现策略
  • Author: Xiao Qiang
  • Created at : 2023-03-05 11:50:50
  • Updated at : 2025-03-08 10:49:30
  • Link: http://fdslk.github.io/tech/java/design-pattern/2023/03/05/easy-rule-demo/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments