如何学习Java的规则引擎模式? - plagov


在这篇博文中,我想描述一下我是如何在为开源项目做出贡献的同时了解规则引擎模式的。
在我作为测试自动化工程师的工作中,我一直在使用 Selenide。所以,当我必须完成某项任务时,我发现 Selenide 没有解决方案来帮助我。所以,我认为这可能是一个为开源库贡献新功能的好机会。
 
问题
我遇到的问题是这样的。Selenide库有一个函数,可以在当前元素的HTML DOM中找到一个父节点。这个父节点是同一个HTML元素,所以它有不同的属性,可以用来在页面上定位它--一个标签名、一个类名、一个属性、一个有值的属性。所以,我们的任务是为每个选项建立一个XPath表达式。

一个非常直接的解决方案是做一个if - else if - else语句。但有了上面提到的四个选项,这种结构就显得太草率了。一定有一个更好、更干净的方法来完成这个任务。
 
规则引擎模式
对于这种问题,确实有一种更好的方法--规则引擎模式。这种模式的本质是将每一个if - else if - else分支分割在其规则类中。然后,主规则引擎类将持有所有的规则,并找到符合客户要求的规则。
 
定义一个规则类
为了确保所有的规则类将实现相同的方法,让我们定义一个接口,每个类将实现这个接口。

public interface AncestorRule {
  Optional<AncestorResult> evaluate(String selector);
}

接下来,让我们定义第一个规则类。该类将容纳if-else分支中定义的逻辑:

public class AncestorWithClassRule implements AncestorRule {

  @Override
  public Optional<AncestorResult> evaluate(String selector) {
    if (isCssClass(selector)) {
      String xpath = format(
        "ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' %s ')][%s]",
        selector.substring(1)
      );
      return Optional.of(new AncestorResult(xpath));
    }
    return Optional.empty();
  }
}

所以,这里是检查给定选择器是否符合给定条件的单一逻辑--如果它是一个CSS类。isCssClass()是一个定义在supper类中的函数(为了简洁起见,这里没有显示)。如果选择器确实是一个CSS类,那么它将建立一个XPath表达式,并将其作为AncestorResult的一个Optional返回,否则就是一个空Optional。

这个规则类是干净的、简短的、容易理解的。它只需写一次,不需要经常修改,除非规则的业务逻辑被更新。

我们以同样的方式定义其他规则。如果输入符合给定的条件,就验证,建立并返回相应的XPath表达式。否则,就是一个空的结果。
 
规则结果
上面的代码有AncestorResult的用法。这个类的目的是包装成功评估计算的结果。这个类看起来如下。

public class AncestorResult {

  private final String value;

  public AncestorResult(String value) {
    this.value = value;
  }

  public String getValue() {
    return value;
  }
}

只有一个类的字段,我们通过构造函数来设置,用getter来访问它。
 
规则引擎类
现在,让我们最后来看看持有规则引擎逻辑的类。

public class AncestorRuleEngine {

    private static final List<AncestorRule> rules = Arrays.asList(
        new AncestorWithTagRule(),
        new AncestorWithClassRule(),
        new AncestorWithAttributeRule(),
        new AncestorWithAttributeAndValueRule()
    );

    public AncestorResult process(String selector) {
        return rules
            .stream()
            .map(rule -> rule.evaluate(selector))
            .flatMap(optional -> optional.map(Stream::of).orElseGet(Stream::empty))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("Selector does not match any rule"));
    }
}


在这个类中做的第一件事是一个适用于这个领域的所有规则的静态列表。如果我们有一个新的规则--我们实现一个新的类并将其添加到这个规则列表中。

第二件事是在所有的规则中处理客户的输入。它对规则列表进行流式处理,评估每一条规则。规则的第一个非空的结果被返回给客户端。否则,规则引擎将抛出一个异常。

关于flatMap()操作的一点。之前的map()函数返回一个Optionals流。然后,flatMap()将一个空的Optionals流转换为一个空流。否则,就转换成非空的AncestorResults流,并将其封装为Optional。该结构与Java 8兼容,看起来很冗长。幸运的是,从Java 9开始,这可以被简化。
 
规则引擎的使用
现在,当我们实现了所有的或规则,规则引擎被定义,让我们看看如何调用和使用这个引擎。

public class ClientSideThatCallsTheRuleEngine {

    public void executeClientCode() {
        // some executions

        AncestorRuleEngine ruleEngine = new AncestorRuleEngine();
        String xpath = ruleEngine.process(selector).getValue();
        
       
// other executions
    }
}

就这么简单。实例化一个规则引擎。传入客户的输入并得到结果。这很干净,简短,精确。我们隐藏了所有验证输入的低级逻辑,建立各自的结果,处理它。与带有多个if - else分支的直截了当的方法相比。我们添加的逻辑越多,这个if - else怪物就越多。